Swift updating UITableView with new data - ios

I'm trying to repopulate my UITableView with data from another JSON call.
However my current setup doesn't seem to work, and while there are many identical questions on SO the answers I could find I've already tried.
I'm saving my API data in CoreData entity objects. And I'm filling my UITableView with my CoreData entities.
In my current setup I have 3 different API Calls that has a different amount of data, and of course different values. I need to be able to switch between these 3 datasets, and that's what I'm trying to accomplish now. (so far without progress).
I have a function called "loadSuggestions", which is where I assume my fault lies.
First I check for an internet connection.
I set the managedObjectContext
I check what API I need to call (This is determined before the function is called, and I checked that it works as intended)
I delete all the current data from the entity that it's trying to call. (I also tried to delete the data from the last data the UITableView had loaded. That didn't change anything). I also checked that this works. After deleting the data, I checked that it prints out an empty array, I also tried logging the objects it deletes to make sure.
I then fetch the new data, save it into temporary variables. Then save it to my core data.
Then I make my second API call (dependant on a variable from the first one), fetch that data and save it the same way.
I append the object to the array the UITableView fills it's cells from. (I checked that it prints out correctly as well)
And lastly I reload the tableView. (doesn't change a thing)
Here's the function:
func loadSuggestions() {
println("----- Loading Data -----")
// Check for an internet connection.
if Reachability.isConnectedToNetwork() == false {
println("ERROR: -> No Internet Connection <-")
} else {
// Set the managedContext again.
managedContext = appDelegate.managedObjectContext!
// Check what API to get the data from
if Formula == 0 {
formulaEntity = "TrialFormulaStock"
println("Setting Entity: \(formulaEntity)")
formulaAPI = NSURL(string: "http://api.com/json/entry_weekly.json")
} else if Formula == 1 {
formulaEntity = "ProFormulaStock"
println("Setting Entity: \(formulaEntity)")
formulaAPI = NSURL(string: "http://api.com/json/entry_weekly.json")
} else if Formula == 2 {
formulaEntity = "PremiumFormulaStock"
formulaAPI = NSURL(string: "http://api.com/json/proff_weekly.json")
println("Setting Entity: \(formulaEntity)")
} else if Formula == 3 {
formulaEntity = "PlatinumFormulaStock"
println("Setting Entity: \(formulaEntity)")
formulaAPI = NSURL(string: "http://api.com/json/fund_weekly.json")
}
// Delete all the current objects in the dataset
let fetchRequest = NSFetchRequest(entityName: formulaEntity)
let a = managedContext.executeFetchRequest(fetchRequest, error: nil) as! [NSManagedObject]
for mo in a {
managedContext.deleteObject(mo)
}
// Removing them from the array
stocks.removeAll(keepCapacity: false)
// Saving the now empty context.
managedContext.save(nil)
// Set up a fetch request for the API data
let entity = NSEntityDescription.entityForName(formulaEntity, inManagedObjectContext:managedContext)
var request = NSURLRequest(URL: formulaAPI!)
var data = NSURLConnection.sendSynchronousRequest(request, returningResponse: nil, error: nil)
var formula = JSON(data: data!)
// Loop through the api data.
for (index: String, actionable: JSON) in formula["actionable"] {
// Save the data into temporary variables
stockName = actionable["name"].stringValue
ticker = actionable["ticker"].stringValue
action = actionable["action"].stringValue
suggestedPrice = actionable["suggested_price"].floatValue
weight = actionable["percentage_weight"].floatValue
// Set up CoreData for inserting a new object.
let stock = NSManagedObject(entity: entity!,insertIntoManagedObjectContext:managedContext)
// Save the temporary variables into coreData
stock.setValue(stockName, forKey: "name")
stock.setValue(ticker, forKey: "ticker")
stock.setValue(action, forKey: "action")
stock.setValue(suggestedPrice, forKey: "suggestedPrice")
stock.setValue(weight, forKey: "weight")
// Get ready for second API call.
var quoteAPI = NSURL(string: "http://dev.markitondemand.com/Api/v2/Quote/json?symbol=\(ticker)")
// Second API fetch.
var quoteRequest = NSURLRequest(URL: quoteAPI!)
var quoteData = NSURLConnection.sendSynchronousRequest(quoteRequest, returningResponse: nil, error: nil)
if quoteData != nil {
// Save the data from second API call to temporary variables
var quote = JSON(data: quoteData!)
betterStockName = quote["Name"].stringValue
lastPrice = quote["LastPrice"].floatValue
// The second API call doesn't always find something, so checking if it exists is important.
if betterStockName != "" {
stock.setValue(betterStockName, forKey: "name")
}
// This can simply be set, because it will be 0 if not found.
stock.setValue(lastPrice, forKey: "lastPrice")
} else {
println("ERROR ----------------- NO DATA for \(ticker) --------------")
}
// Error handling
var error: NSError?
if !managedContext.save(&error) {
println("Could not save \(error), \(error?.userInfo)")
}
// Append the object to the array. Which fills the UITableView
stocks.append(stock)
}
// Reload the tableview with the new data.
self.tableView.reloadData()
}
}
Currently, when I push to this viewController, this function is called in viewDidAppear like so:
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(true)
tableView.allowsSelection = true
if isFirstTime {
loadSuggestions()
isFirstTime = false
}
}
It populates the tableView correctly and everything seems to work as planned.
However if I open my slide-out menu and call a function to load different data, nothing happens, here's an example function:
func platinumFormulaTapGesture() {
// Menu related actions
selectView(platinumFormulaView)
selectedMenuItem = 2
// Setting the data to load
Formula = 3
// Sets the viewController. (this will mostly be the same ViewController)
menuTabBarController.selectedIndex = 0
// Set the new title
navigationController?.navigationBar.topItem!.title = "PLATINUM FORMULA"
// And here I call the loadSuggestions function again. (this does run)
SuggestionsViewController().loadSuggestions()
}
Here's the 2 relevant tableView functions:
number of Rows:
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return stocks.count
}
And cellForRowAtIndexPath, (this is where I set up my cells with the CoreData)
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("com.mySuggestionsCell", forIndexPath: indexPath) as! mySuggestionsCell
let formulaStock = stocks[indexPath.row]
cell.stockNameLabel.text = formulaStock.valueForKey("name") as! String!
cell.tickerLabel.text = formulaStock.valueForKey("ticker") as! String!
action = formulaStock.valueForKey("action") as! String!
suggestedPrice = formulaStock.valueForKey("suggestedPrice") as! Float
let suggestedPriceString = "Suggested Price\n$\(suggestedPrice.roundTo(2))" as NSString
var suggestedAttributedString = NSMutableAttributedString(string: suggestedPriceString as String)
suggestedAttributedString.addAttributes(GrayLatoRegularAttribute, range: suggestedPriceString.rangeOfString("Suggested Price\n"))
suggestedAttributedString.addAttributes(BlueHalisRBoldAttribute, range: suggestedPriceString.rangeOfString("$\(suggestedPrice.roundTo(2))"))
cell.suggestedPriceLabel.attributedText = suggestedAttributedString
if action == "SELL" {
cell.suggestionContainer.backgroundColor = UIColor.formulaGreenColor()
}
if let lastPrice = formulaStock.valueForKey("lastPrice") as? Float {
var lastPriceString = "Last Price\n$\(lastPrice.roundTo(2))" as NSString
var lastAttributedString = NSMutableAttributedString(string: lastPriceString as String)
lastAttributedString.addAttributes(GrayLatoRegularAttribute, range: lastPriceString.rangeOfString("Last Price\n"))
percentDifference = ((lastPrice/suggestedPrice)*100.00)-100
if percentDifference > 0 && action == "BUY" {
lastAttributedString.addAttributes(RedHalisRBoldAttribute, range: lastPriceString.rangeOfString("$\(lastPrice.roundTo(2))"))
} else if percentDifference <= 0 && percentDifference > -100 && action == "BUY" {
lastAttributedString.addAttributes(GreenHalisRBoldAttribute, range: lastPriceString.rangeOfString("$\(lastPrice.roundTo(2))"))
} else if percentDifference <= 0 && percentDifference > -100 && action == "SELL" {
lastAttributedString.addAttributes(RedHalisRBoldAttribute, range: lastPriceString.rangeOfString("$\(lastPrice.roundTo(2))"))
} else if percentDifference == -100 {
lastPriceString = "Last Price\nN/A" as NSString
lastAttributedString = NSMutableAttributedString(string: lastPriceString as String)
lastAttributedString.addAttributes(GrayLatoRegularAttribute, range: lastPriceString.rangeOfString("Last Price\n"))
lastAttributedString.addAttributes(BlackHalisRBoldAttribute, range: lastPriceString.rangeOfString("N/A"))
}
cell.lastPriceLabel.attributedText = lastAttributedString
} else {
println("lastPrice nil")
}
weight = formulaStock.valueForKey("weight") as! Float
cell.circleChart.percentFill = weight
let circleChartString = "\(weight.roundTo(2))%\nWEIGHT" as NSString
var circleChartAttributedString = NSMutableAttributedString(string: circleChartString as String)
circleChartAttributedString.addAttributes(BlueMediumHalisRBoldAttribute, range: circleChartString.rangeOfString("\(weight.roundTo(2))%\n"))
circleChartAttributedString.addAttributes(BlackSmallHalisRBoldAttribute, range: circleChartString.rangeOfString("WEIGHT"))
cell.circleChartLabel.attributedText = circleChartAttributedString
cell.selectionStyle = UITableViewCellSelectionStyle.None
return cell
}
I define my appDelegate as the very first thing in my class:
let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
var managedContext = NSManagedObjectContext()
I think that's all the code that could possibly be the cause of the bug. Again I think the most likely cause would be in the loadSuggestions function.
To force update the tableView I also tried calling setNeedsDisplay and setNeedsLayout both on self.view and tableView, neither of which seemed to do anything at all.
Any advice in figuring out why this tableView refuses to update would be an enormous help!
And I apologize for the walls of code, but I havn't been able to find the exact origin of the issue.

This line in the platinumFormulaTapGesture function is incorrect,
SuggestionsViewController().loadSuggestions()
This creates a new instance of SuggestionsViewController, which is not the one you have on screen. You need to get a pointer to the one you have. How you do that depends on your controller hierarchy, which you haven't explained fully enough.

Related

Issue trying to complete Firebase Storage download before showing tableview

I have a table view where depending on the cell class it will download an image from Firebase. I've noticed when using the app that cells with the same cell identifier will show the previous downloaded image before showing the new one. This is what I have before changing it.
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if tableData[indexPath.row]["Image"] != nil {
let cell = tableView.dequeueReusableCell(withIdentifier: "imageNotesData", for: indexPath) as! ImageNotesCell
cell.notes.delegate = self
cell.notes.tag = indexPath.row
cell.notes.text = tableData[indexPath.row]["Notes"] as! String
guard let imageFirebasePath = tableData[indexPath.row]["Image"] else {
return cell }
let pathReference = Storage.storage().reference(withPath: imageFirebasePath as! String)
pathReference.getData(maxSize: 1 * 1614 * 1614) { data, error in
if let error = error {
print(error)
} else {
let image = UIImage(data: data!)
cell.storedImage.image = image
}
}
return cell
}
else {
let cell = tableView.dequeueReusableCell(withIdentifier: "notesData", for: indexPath) as! NotesCell
//let noteString = tableData[indexPath.row]["Notes"] as! String
cell.notes.text = tableData[indexPath.row]["Notes"] as! String
cell.notes.delegate = self
cell.notes.tag = indexPath.row
return cell
}
}
Knowing that this is not a good user experience and that it looks clunky, I tried to move the pathReference.getData to where I setup the data but the view appears before my images finish downloading. I have tried to use a completion handler but I'm still having issues.
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(true)
getSectionData(userID: userID, city: selectedCity, completion: {(sectionString) in
self.setupTableCellView(userID: userID, city: selectedCity, section: sectionString) { (tableData) in
DispatchQueue.main.async(execute: {
self.cityName?.text = selectedCity
self.changeSections.setTitle(sectionString, for: .normal)
self.currentSectionString = sectionString
self.setupTableData(tableDataHolder: tableData)
})
}
})
}
func setupTableCellView(userID: String, city: String, section: String, completion: #escaping ([[String:Any]]) -> () ) {
let databaseRef = Database.database().reference().child("Users").child(userID).child("Cities").child(city).child(section)
var indexData = [String:Any]()
var indexDataArray = [[String:Any]]()
databaseRef.observeSingleEvent(of: .value, with: { (snapshot) in
for dataSet in snapshot.children {
let snap = dataSet as! DataSnapshot
//let k = snap.key
let v = snap.value
indexData = [:]
for (key, value) in v as! [String: Any] {
//indexData[key] = value
if key == "Image" {
//let pathReference = Storage.storage().reference(withPath: value as! String)
print("before getImageData call")
self.getImageData(pathRef: value as! String, completion: {(someData) in
print("before assigning indexData[key]")
indexData[key] = someData
print("after assigning indexData[key]")
})
} else {
indexData[key] = value
}
}
indexDataArray.append(indexData)
}
completion(indexDataArray)
})
}
func getImageData(pathRef: String, completion: #escaping(UIImage) -> ()) {
let pathReference = Storage.storage().reference(withPath: pathRef as! String)
pathReference.getData(maxSize: 1 * 1614 * 1614, completion: { (data, error) in
if let error = error {
print(error)
} else {
let image = UIImage(data:data!)
print("called before completion handler w/ image")
completion(image!)
}
})
}
I don't know if I am approaching this the right way but I think I am. I'm also guessing that the getData call is async and that is why it will always download after showing the table view.
You can't do this.
Make the request from Firebase.
Over time, you will get many replies - all the information and all the changing information.
When each new item arrives - and don't forget it may be either an addition or deletion - alter your table so that it displays all the current items.
That's OCC!
OCC is "occasionally connected computing". A similar phrase is "offline first computing". So, whenever you use any major service you use every day like Facebook, Snapchat, etc that is "OCC": everything stays in sync properly whether you do or don't have bandwidth. You know? The current major paradigm of device-cloud computing.
Edit - See Fattie's comments about prepareForReuse()!
With reusable table cells, the cells will at first have the appearance they do by default / on the xib. Once they're "used", they have whatever data they were set to. This can result in some wonky behavior. I discovered an issue where in my "default" case from my data, I didn't do anything ecause it already matched the xib, but if the data's attributes were different, I updated the appearance. The result was that scrolling up and down really fast, some things that should have had the default appearance had the changed appearance.
One basic solution to just not show the previous image would be to show a place holder / empty image, then call your asynchronous fetch of the image. Not exactly what you want because the cell will still show up empty...
Make sure you have a local store for the images, otherwise you're going to be making a server request for images you already have as you scroll up and down!
I'd recommend in your viewDidLoad, call a method to fetch all of your images at once, then, once you have them all, in your success handler, call self.tableview.reloadData() to display it all.

Cloudkit Fetch very slow

Running the below code to fetch data from Cloudkit, at the moment it is taking a long to populate a tableView, depending on how many results there are, but if there are over 15 results it takes 10 seconds plus. Are they any ways I can speed this up?
This is my fetch func:
func loadData() {
venues = [CKRecord]()
let location = locationManager.location
let radius = CLLocationDistance(500)
let sort = CKLocationSortDescriptor(key: "Location", relativeLocation: location!)
let predicate = NSPredicate(format: "distanceToLocation:fromLocation:(%K,%#) < %f", "Location", location!, radius)
let publicData = CKContainer.defaultContainer().publicCloudDatabase
let query = CKQuery(recordType: "Venues", predicate: predicate )
query.sortDescriptors = [sort]
publicData.performQuery(query, inZoneWithID: nil) { (results:[CKRecord]?, error:NSError?) in
if let venues = results {
self.venues = venues
dispatch_async(dispatch_get_main_queue(), {
self.tableView.reloadData()
self.refreshControl.endRefreshing()
self.tableView.hidden = false
})
}
}
}
This is my tableView func:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath) as! NearMe2ViewCell
if venues.count == 0 {
return cell
}
let venue = venues[indexPath.row]
print(indexPath.row)
let venueLocation = venue["Location"] as? CLLocation
let venueTitle = (venue["Name"] as! String)
let venueImages = venue["VenuePhoto"] as! CKAsset
let userLocation = locationManager.location
let distanceBetween: CLLocationDistance = (venueLocation!.distanceFromLocation(userLocation!))
self.venueDistance = String(format: "%.f", distanceBetween)
cell.venueDistance?.text = venueDistance
cell.venueName.text = venueTitle
cell.venueImage?.image = UIImage(contentsOfFile: venueImages.fileURL.path!)
return cell
}
You should search for the record keys first, so a fetchOperation would include this directive.
fetchOperation.desiredKeys = ["record.recordID.recordName"]
That should be faster. Break your returned keys into the size you can display on the screen and go get them only. After you display them, go get the next batch in background thread, when you got that the next batch on background etc etc etc.
Should add perhaps, that fetching the asset should be done on a separate thread too if possible, updating the table as you pull in the assets by reloading the table repeatedly.
Here is method to search and return keys.
func zap(theUUID:String) {
var recordID2Zap: String!
let predicate = NSPredicate(format: "(theUUID = %#)",theUUID)
let query = CKQuery(recordType: "Blah", predicate: predicate)
let searchOperation = CKQueryOperation(query: query)
searchOperation.desiredKeys = ["record.recordID.recordName"]
searchOperation.recordFetchedBlock = { (record) in
recordID2Zap = record.recordID.recordName
}
if error != nil {
print("ting, busted",error!.localizedDescription)
} else {
print("ok zapping")
if recordID2Zap != nil {
self.privateDB.delete(withRecordID: CKRecordID(recordName: recordID2Zap), completionHandler: {recordID, error in
NSLog("OK or \(error)")
})
}
}
}
searchOperation.qualityOfService = .background
privateDB.add(searchOperation)
theApp.isNetworkActivityIndicatorVisible = true
}
}
As for your tableview, and images... use the completion in your icloud code to send a notification to the table view.
database.fetchRecordWithID(CKRecordID(recordName: recordId), completionHandler: {record, error in
let directDict = ["blah": "whatever"] as [String : String]
NotificationCenter.default.post(name: Notification.Name("blahDownloaded"), object: nil, userInfo: directDict)
}
And in the VC you register said notification.
NotificationCenter.default.addObserver(self, selector: #selector(blahDownloaded), name: Notification.Name("blahDownloaded"), object: nil)
func blahDownloaded(notification: NSNotification) {
if let userInfo = notification.userInfo as NSDictionary? as? [String: Any] {
//update you cell
//reload your table
}
Does that all make sense?
Your operation's qualityOfService is defaulting to .utility.
There is an important note in the documentation for CKOperation that states:
CKOperation objects have a default quality of service level of NSQualityOfServiceUtility (see qualityOfService). Operations at this level are considered discretionary, and are scheduled by the system for an optimal time based on battery level and other factors.
Because CKOperation inherits from NSOperation you can configure the qualityOfService property when your user is waiting on a request to finish. Here is some example code based off of yours above:
let queryOperation = CKQueryOperation(query: query)
queryOperation.recordFetchedBlock = ...
queryOperation.queryCompletionBlock = ...
queryOperation.qualityOfService = .userInteractive
publicData.add(queryOperation)
Notice that this example explicitly creates a CKQueryOperation instead of using the convenience API because it then gives you the flexibility to fully configure your operations before you enqueue them to be sent to the server.
In this case you can set the qualityOfService to .userInteractive because your user is actively waiting on the request to finish before they can use your app any further. Learn more about the possible values at https://developer.apple.com/library/content/documentation/Performance/Conceptual/EnergyGuide-iOS/PrioritizeWorkWithQoS.html

How to delay a return call in Swift

Hello guys I am currently working on a program that holds a list of books in a UITableView. As you know, the TableView takes two methods, one with cellForRowAtIndexPath and the one I will be talking about today, numberOfRowsInSection. So the problem I am having is that I access my database to get the number of books that are currently in the database in order to return how many indices I will need in the array of Book stucts. So I have two groups, buy and sell, that may or may not have any books in them.
Anyway, I populate my array (it's empty to start with) and then I return the books.count as the numberOfRowsInSection. The problem is that I am consistently returning 0 seeing as the array gets populated after the return is executed.
Below is my code.
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
populateArray()
print("books.count: ",books.count)
return books.count // KEEPS RETURNING 0 BC IT HASN'T POPULATED YET *ARRRRRRGH*
}
func populateArray(){
print("started looking")
var indices = 0
if divider.selectedSegmentIndex == 0{
ref.child(school).observeEventType(.Value, withBlock: { (snapshot) in
let numSelling = snapshot.value!["numSelling"] as! Int // gets numSelling
if numSelling > 0 {
self.noEntries = false
print("numSelling: ", numSelling) //see console
indices = numSelling
}else{
self.noEntries = true
indices = 1
print("No Values Selling")
}
}) { (error) in
print(error.localizedDescription)
}
}else{
ref.child(school).observeEventType(.Value, withBlock: { (snapshot) in
let numBuying = snapshot.value!["numBuying"] as! Int // gets numBuying
if numBuying > 0 {
self.noEntries = false
print("numBuying: ", numBuying) //see console
indices = numBuying
}else{
self.noEntries = true
indices = 1
}
}) { (error) in
print(error.localizedDescription)
}
}
delay(0.5){
print("ind: ", indices) // printing correctly
if(self.noEntries){ // just add one book to get the indices to be 1
self.books.append(Book(isbn: "", title: "", author: "", edition: "", price: "", uid: ""))
return
}
if self.divider.selectedSegmentIndex == 0{
self.ref.child(self.school).child("selling").observeEventType(.Value, withBlock: { (snapshot) in
let booksJSON = snapshot.value! as! NSArray
for bookJSON in booksJSON { // add the book to the array
let tempAuthor = bookJSON["authors"] as! String
let tempTitle = bookJSON["title"] as! String
let tempEdition = bookJSON["edition"] as! String
let tempPrice = bookJSON["price"] as! String
let tempISBN = bookJSON["isbn"] as! String
let tempUID = bookJSON["uid"] as! String
self.books.append(Book(isbn: tempISBN, title: tempTitle, author: tempAuthor, edition: tempEdition, price: tempPrice, uid: tempUID))
}
}) { (error) in
print(error.localizedDescription)
}
}else if self.divider.selectedSegmentIndex == 1{
self.ref.child(self.school).child("buying").observeEventType(.Value, withBlock: { (snapshot) in
let booksJSON = snapshot.value! as! NSArray
for bookJSON in booksJSON { // add the book to the array
let tempAuthor = bookJSON["authors"] as! String
let tempTitle = bookJSON["title"] as! String
let tempEdition = bookJSON["edition"] as! String
let tempPrice = bookJSON["price"] as! String
let tempISBN = bookJSON["isbn"] as! String
let tempUID = bookJSON["uid"] as! String
self.books.append(Book(isbn: tempISBN, title: tempTitle, author: tempAuthor, edition: tempEdition, price: tempPrice, uid: tempUID))
}
}) { (error) in
print(error.localizedDescription)
}
}
}
}
func delay(delay:Double, closure:()->()) {
dispatch_after(
dispatch_time(
DISPATCH_TIME_NOW,
Int64(delay * Double(NSEC_PER_SEC))
),
dispatch_get_main_queue(), closure)
}
Keep in mind that I cannot make a callback in this method because it is automatically called by the program when the view is loaded.
Also, the delay segments are in efforts to stop the same thing from happening. Problem is that I cannot put the delay around the return because it thinks I want to return an Int for the delay block.
Console:
started looking
books.count: 0
started looking
books.count: 0
started looking
books.count: 0
started looking
books.count: 0
numSelling: 6
numSelling: 6
numSelling: 6
numSelling: 6
ind: 6
ind: 6
ind: 6
ind: 6
As you can see it is returning 0 before it even gets to the numSelling value from the database.
Thank you so much for your help and have a great day!
You cannot delay returning to a method once it has been called, but you can ask the table view to call the data source methods again.
The easiest solution would be to call reloadData() on your table view once your data has been populated (i.e., at the end of your populateArray() method). I would probably also move the call to populateArray() somewhere else (perhaps viewDidLoad(), if that's appropriate).
I would:
Move the "retrieve titles being bought/sold" out of the delay method. Call that from within the respective observeEventType of numSelling/numBuying. Get rid of the delay.
As Charles says, don't worry that the values are empty when viewDidLoad finishes. Just have the routine call tableView.reloadData() when it's done.
Assuming your UI is showing both titles being bought and sold at the same time (or that you're jumping between them quickly and don't want to wait for the data to be retrieved), your routine might want to go ahead and retrieve both arrays and only call reloadData when both are done. This means that you might need two arrays in your model, one for booksSelling and one for booksBuying.

Swift iOS: Firebase Paging

I have this Firebase data:
I want to query the posts data through pagination. Currently my code is converting this JS code to Swift code
let postsRef = self.rootDatabaseReference.child("development/posts")
postsRef.queryOrderedByChild("createdAt").queryStartingAtValue((page - 1) * count).queryLimitedToFirst(UInt(count)).observeSingleEventOfType(.Value, withBlock: { snapshot in
....
})
When accessing, this data page: 1, count: 1. I can get the data for "posts.a" but when I try to access page: 2, count: 1 the returns is still "posts.a"
What am I missing here?
Assuming that you are or will be using childByAutoId() when pushing data to Firebase, you can use queryOrderedByKey() to order your data chronologically. Doc here.
The unique key is based on a timestamp, so list items will automatically be ordered chronologically.
To start on a specific key, you will have to append your query with queryStartingAtValue(_:).
Sample usage:
var count = numberOfItemsPerPage
var query ref.queryOrderedByKey()
if startKey != nil {
query = query.queryStartingAtValue(startKey)
count += 1
}
query.queryLimitedToFirst(UInt(count)).observeSingleEventOfType(.Value, withBlock: { snapshot in
guard var children = snapshot.children.allObjects as? [FIRDataSnapshot] else {
// Handle error
return
}
if startKey != nil && !children.isEmpty {
children.removeFirst()
}
// Do something with children
})
I know I'm a bit late and there's a nice answer by timominous, but I'd like to share the way I've solved this. This is a full example, it isn't only about pagination. This example is in Swift 4 and I've used a nice library named CodableFirebase (you can find it here) to decode the Firebase snapshot values.
Besides those things, remember to use childByAutoId when creating a post and storing that key in postId(or your variable). So, we can use it later on.
Now, the model looks like so...
class FeedsModel: Decodable {
var postId: String!
var authorId: String! //The author of the post
var timestamp: Double = 0.0 //We'll use it sort the posts.
//And other properties like 'likesCount', 'postDescription'...
}
We're going to get the posts in the recent first fashion using this function
class func getFeedsWith(lastKey: String?, completion: #escaping ((Bool, [FeedsModel]?) -> Void)) {
let feedsReference = Database.database().reference().child("YOUR FEEDS' NODE")
let query = (lastKey != nil) ? feedsReference.queryOrderedByKey().queryLimited(toLast: "YOUR NUMBER OF FEEDS PER PAGE" + 1).queryEnding(atValue: lastKey): feedsReference.queryOrderedByKey().queryLimited(toLast: "YOUR NUMBER OF FEEDS PER PAGE")
//Last key would be nil initially(for the first page).
query.observeSingleEvent(of: .value) { (snapshot) in
guard snapshot.exists(), let value = snapshot.value else {
completion(false, nil)
return
}
do {
let model = try FirebaseDecoder().decode([String: FeedsModel].self, from: value)
//We get the feeds in ['childAddedByAutoId key': model] manner. CodableFirebase decodes the data and we get our models populated.
var feeds = model.map { $0.value }
//Leaving the keys aside to get the array [FeedsModel]
feeds.sort(by: { (P, Q) -> Bool in P.timestamp > Q.timestamp })
//Sorting the values based on the timestamp, following recent first fashion. It is required because we may have lost the chronological order in the last steps.
if lastKey != nil { feeds = Array(feeds.dropFirst()) }
//Need to remove the first element(Only when the lastKey was not nil) because, it would be the same as the last one in the previous page.
completion(true, feeds)
//We get our data sorted and ready here.
} catch let error {
print("Error occured while decoding - \(error.localizedDescription)")
completion(false, nil)
}
}
}
Now, in our viewController, for the initial load, the function calls go like this in viewDidLoad. And the next pages are fetched when the tableView will display cells...
class FeedsViewController: UIViewController {
//MARK: - Properties
#IBOutlet weak var feedsTableView: UITableView!
var dataArray = [FeedsModel]()
var isFetching = Bool()
var previousKey = String()
var hasFetchedLastPage = Bool()
//MARK: - ViewController LifeCycle
override func viewDidLoad() {
super.viewDidLoad()
//Any other stuffs..
self.getFeedsWith(lastKey: nil) //Initial load.
}
//....
func getFeedsWith(lastKey: String?) {
guard !self.isFetching else {
self.previousKey = ""
return
}
self.isFetching = true
FeedsModel.getFeedsWith(lastKey: lastKey) { (status, data) in
self.isFetching = false
guard status, let feeds = data else {
//Handle errors
return
}
if self.dataArray.isEmpty { //It'd be, when it's the first time.
self.dataArray = feeds
self.feedsTableView.reloadSections(IndexSet(integer: 0), with: .fade)
} else {
self.hasFetchedLastPage = feeds.count < "YOUR FEEDS PER PAGE"
//To make sure if we've fetched the last page and we're in no need to call this function anymore.
self.dataArray += feeds
//Appending the next page's feed. As we're getting the feeds in the recent first manner.
self.feedsTableView.reloadData()
}
}
}
//MARK: - TableView Delegate & DataSource
//....
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
if self.dataArray.count - 1 == indexPath.row && !self.hasFetchedLastPage {
let lastKey = self.dataArray[indexPath.row].postId
guard lastKey != self.previousKey else { return }
//Getting the feeds with last element's postId. (postId would be the same as a specific node in YourDatabase/Feeds).
self.getFeedsWith(lastKey: lastKey)
self.previousKey = lastKey ?? ""
}
//....
}

swift, removing objects from core data, still appends empty objects?

I'm attempting to set up a UITableView, with all objects in an entity.
However, I'm loading the data from an api. So every time it loads, I'm deleting all the objects in the entity. And adding the new ones.
However, when I do this. It's showing 5 cells with the api data, and every time I load it. It adds 5 empty cells.
The reason it's adding empty cells is because I'm defining numberOfRowsInSection with objects count like so:
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return stocks.count
}
Where stocks is: var stocks = [NSManagedObject]()
So from that I assume it's because there's somehow empty objects in my entity?
here's how I'm trying to set up my data:
func loadSuggestions(Formula: Int) {
println("----- Loading Data -----")
// Check for an internet connection
if Reachability.isConnectedToNetwork() == false {
println("ERROR: -> No Internet Connection <-")
} else {
// Delete all the current objects in the entity
let fetchRequest = NSFetchRequest(entityName: formulaEntity)
let a = managedContext.executeFetchRequest(fetchRequest, error: nil) as! [NSManagedObject]
for mo in a {
managedContext.deleteObject(mo)
//println("removed \(mo)")
}
// save the context after removing objects.
managedContext.save(nil)
// Setting up a fetch request to get the api data.
let entity = NSEntityDescription.entityForName("\(formulaEntity)", inManagedObjectContext:managedContext)
var request = NSURLRequest(URL: formulaAPI!)
var data = NSURLConnection.sendSynchronousRequest(request, returningResponse: nil, error: nil)
var formula = JSON(data: data!)
for (index: String, actionable: JSON) in formula["actionable"] {
stockName = actionable["name"].stringValue
ticker = actionable["ticker"].stringValue
action = actionable["action"].stringValue
suggestedPrice = actionable["suggested_price"].floatValue
weight = actionable["percentage_weight"].floatValue
// Set the values in CoreData
let stock = NSManagedObject(entity: entity!,insertIntoManagedObjectContext:managedContext)
stock.setValue(stockName, forKey: "name")
stock.setValue(ticker, forKey: "ticker")
stock.setValue(action, forKey: "action")
stock.setValue(suggestedPrice, forKey: "suggestedPrice")
stock.setValue(weight, forKey: "weight")
// Set up second api fetch
var quoteAPI = NSURL(string: "http://dev.markitondemand.com/Api/v2/Quote/json?symbol=\(ticker)")
var quoteRequest = NSURLRequest(URL: quoteAPI!)
var quoteData = NSURLConnection.sendSynchronousRequest(quoteRequest, returningResponse: nil, error: nil)
if quoteData != nil {
var quote = JSON(data: quoteData!)
betterStockName = quote["Name"].stringValue
lastPrice = quote["LastPrice"].floatValue
// Setting the last values in CoreData
if betterStockName != "" {
stock.setValue(betterStockName, forKey: "name")
}
stock.setValue(lastPrice, forKey: "lastPrice")
} else {
println("ERROR - NO DATA")
}
var error: NSError?
if !managedContext.save(&error) {
println("Could not save \(error), \(error?.userInfo)")
}
// finally add the stockdata to the CoreData entity.
stocks.append(stock)
}
// Reload the tableview with the new data.
self.tableView.reloadData()
}
}
There isn't a lot of information on how to delete all of the objects in an entity. But I tried to print out the objects it was deleting, and that all seemed correct.
But I must be doing something wrong somewhere in the above function. Since it adds 5 more cells every time the above function is called.
Any help figuring out where the 5 empty cells are coming from and how to fix it would be greatly appreciated!
The simple reason is that you only ever add new entries to stocks, you never remove anything. If you never remove anything from the array, it will never get smaller.
You do delete the objects from Core Data, but that doesn't automatically remove them from the array. They don't disappear from memory and an array just because Core Data doesn't have them anymore. You need to do that separately by calling removeAll on the array after deleting the objects from Core Data.

Resources