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
Related
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.
I'm working with a mapview not a tableview, but I don't know what to use to replace indexPath.row.
I have a mapview with annotations, when the info button of an annotation is pressed I then query my CK database and return the record that has a name field matching the name of the annotation pressed. This returns an [CKRecord] with a single record, as there are no matching names.
At this point, with a tableview I would do the following to access the data...
let placeInfo = selectedData[indexPath.row]
let placeName = placeInfo.objectForKey("Name") as! String
let placeCity = placeInfo.objectForKey("City") as! String
However, since I'm not using a tableview, I don't have an indexPath to use. Since my [CKRecord] object only contains a single record, I thought I could replace indexPath.row with the array location of the record...
let placeInfo = selectedPlace[0] //also tried 1
That lines produces an Index out of range error.
I've tried everything that I know, and as you may imagine, I'm not exactly great at swift or programming in general at this point.
Here is the full mapView function I am using...
func mapView(mapView: MKMapView, annotationView: MKAnnotationView, calloutAccessoryControlTapped control: UIControl) {
let cloudContainer = CKContainer.defaultContainer()
let publicData = cloudContainer.publicCloudDatabase
let tappedPlace = annotationView.annotation!.title!! as String
let predi = NSPredicate(format: "Name = %#", tappedPlace)
let iquery = CKQuery(recordType: "Locations", predicate: predi)
publicData.performQuery(iquery, inZoneWithID: nil, completionHandler: {
(results, error) -> Void in
if error != nil {
print(error)
return
}
if let results = results {
print("Downloaded data for selected location for \(tappedPlace)")
NSOperationQueue.mainQueue().addOperationWithBlock() {
self.selectedPlace = results
}
}
})
let placeInfo = selectedPlace[0]
let placeName = placeInfo.objectForKey("Name") as! String
//returns Index out of range error for placeInfo line
//need data before segue
//performSegueWithIdentifier("fromMap", sender: self)
}
Your problem is that, you try to access selectedPlace before it is actually signed by your completion handler. Your 'publicData.performQuery' seems to be an asynchronous operation, and this means that, the control will come out from this call even before the completion handler gets executed(this is expected in case of an asynchronous call). And you reach the line immediately-
let placeInfo = selectedPlace[0]
But the data is not ready yet, and you get the exception. Now to solve this, move place info extraction, and perform segue code inside the completion handler as shown-
publicData.performQuery(iquery, inZoneWithID: nil, completionHandler: {
(results, error) -> Void in
if error != nil {
print(error)
return
}
if let results = results {
print("Downloaded data for selected location for \(tappedPlace)")
NSOperationQueue.mainQueue().addOperationWithBlock() {
self.selectedPlace = results
if(results.count > 0){
let placeInfo = selectedPlace[0]
let placeName = placeInfo.objectForKey("Name") as! String
//Do any other computations as needed.
performSegueWithIdentifier("fromMap", sender: self)
}
}
}
})
This should fix your problem.
I'm moving this getCloudKit function from ViewController.swift to Lay.swift so I can keep everything in a single class.
var objects = [Lay]()
override func viewDidLoad() {
super.viewDidLoad()
self.refreshControl?.addTarget(self, action: "handleRefresh:", forControlEvents: UIControlEvents.ValueChanged)
self.getCloudKit()
}
func handleRefresh(refreshControl: UIRefreshControl) {
self.objects.removeAll()
self.getCloudKit()
}
func getCloudKit() {
let now = NSDate()
let predicate = NSPredicate(format: "TimeDate > %#", now)
let sort = NSSortDescriptor(key: "TimeDate", ascending: true)
let container = CKContainer.defaultContainer()
let publicData = container.publicCloudDatabase
let query = CKQuery(recordType: “lay”, predicate: predicate)
query.sortDescriptors = [sort]
publicData.performQuery(query, inZoneWithID: nil) { results, error in
if error == nil {
for lay in results! {
let newlay = Lay()
newLay.tColor = lay["tColor"] as! String
newLay.timeDate = lay["TimeDate"] as! AnyObject
newLay.matchup = lay["Matchup"] as! String
let applicationDict = ["tColor" : newLay.tColor, "Matchup" : newLay.matchup]
let transfer = WCSession.defaultSession().transferUserInfo(applicationDict)
self.objects.append(newLay)
}
dispatch_async(dispatch_get_main_queue(), { () -> Void in
self.refreshControl!.endRefreshing()
self.tableView.reloadData()
})
} else {
print(error)
}
}
}
The problem is when I move it (and the necessary related code):
Error in Lay.swift on TableViewController().refreshControl!.endRefreshing()
saying "fatal error: unexpectedly found nil while unwrapping an
Optional value"
Need to put my WCSession: transferUserInfo code from getCloudKit in my AppDelegate.swift, but keep getting errors when I try
New ViewController.swift:
override func viewDidLoad() {
super.viewDidLoad()
self.refreshControl?.addTarget(self, action: "handleRefresh:", forControlEvents: UIControlEvents.ValueChanged)
Lay().getCloudKit()
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// #warning Incomplete implementation, return the number of rows
return Lay().objects.count
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("reuseIdentifier", forIndexPath: indexPath)
let object = Lay().objects[indexPath.row];
if let label = cell.textLabel{
label.text = object.matchup
}
return cell
}
func handleRefresh(refreshControl: UIRefreshControl) {
Lay().objects.removeAll()
Lay().getCloudKit()
}
New Lay.swift:
var objects = [Lay]()
func getCloudKit() {
let now = NSDate()
let predicate = NSPredicate(format: "TimeDate > %#", now)
let sort = NSSortDescriptor(key: "TimeDate", ascending: true)
let container = CKContainer.defaultContainer()
let publicData = container.publicCloudDatabase
let query = CKQuery(recordType: “lay”, predicate: predicate)
query.sortDescriptors = [sort]
publicData.performQuery(query, inZoneWithID: nil) { results, error in
if error == nil {
for lay in results! {
let newlay = Lay()
newLay.tColor = lay["tColor"] as! String
newLay.timeDate = lay["TimeDate"] as! AnyObject
newLay.matchup = lay["Matchup"] as! String
let applicationDict = ["tColor" : newlay.tColor, "Matchup" : newlay.matchup]
let transfer = WCSession.defaultSession().transferUserInfo(applicationDict)
self.objects.append(newlay)
}
dispatch_async(dispatch_get_main_queue(), { () -> Void in
TableViewController().refreshControl!.endRefreshing()
TableViewController().tableView.reloadData()
})
} else {
print(error)
}
}
New AppDelegate:
private func setupWatchConnectivity() {
if WCSession.isSupported() {
let session = WCSession.defaultSession()
session.delegate = self
session.activateSession()
}
}
private func sendUpdatedDataToWatch(notification: NSNotification) {
if WCSession.isSupported() {
let session = WCSession.defaultSession()
if session.watchAppInstalled
{
let applicationDict = ["TColor" : Lay().tColor, "Matchup" : Lay().matchup]
let transfer = WCSession.defaultSession().transferUserInfo(applicationDict)
NSLog("Transfer AppDelegate: %#", transfer)
NSLog("Trans AppDelegate: %#", applicationDict)
session.transferCurrentComplicationUserInfo(applicationDict)
}
}
}
Your code has ViewController() and Lay() throughout. This will create new instances of those objects. Therefore, although refreshControl is non-nil in your actual view controller, it will be nil in a newly created one.
By splitting out the getCloudKit function, you're allowing the view controller to just manage the view, and the new class to just manage Cloud Kit. This is good, so ideally your Cloud Kit controller should not know anything about the view controller. Therefore, getCloudKit shouldn't be calling reloadData. Instead, you could pass a closure into getCloudKit that gets called when the query finishes. Something along the lines of:
func getCloudKit(completion completionHandler: (([Lay]) -> Void)?) {
...
publicData.performQuery(query, inZoneWithID: nil) { results, error in
if error == nil {
...
if let completion = completionHandler {
completion(self.objects)
}
} else {
print(error)
}
}
Then in ViewController:
let layCloudKit = LayCloudKit()
layCloudKit.getCloudKit(completion: { (objects) -> Void in
dispatch_async(dispatch_get_main_queue(), {
self.objects = objects
self.refreshControl!.endRefreshing()
self.tableView.reloadData()
})
})
Note that I've assumed you would put the Lay Cloud Kit controller into a separate Swift file, as the Lay model class shouldn't need to know about Cloud Kit. If you want to put it in the same file as Lay, then you should mark the func as static or class, because you don't want to create a dummy instance of Lay just to call getCloudKit. In that case, you would call it using Lay.getCloudKit (ie. specifying the Lay class, rather than a Lay instance).
I'm writing an app in Swift where the first scene has a TableView, I have it setup to display the title and it works fine, I also have it setup to count occurrences in a CloudKit database(or whatever its called) but it performs the count in async so the table defaults to show 0 in the detail pane.
I need to know how to make the app wait before it sets the value for the right detail until the count is completed or how to change them afterwards.
I have attached the code I used to perform the count etc, if I am doing this wrong or inefficiently please let me know
override func viewDidLoad() {
super.viewDidLoad()
self.navigationItem.hidesBackButton = true;
self.textArray.addObject("Link 300")
self.textArray.addObject("Link 410")
self.textArray.addObject("Link 510")
let container = CKContainer.defaultContainer()
let publicData = container.publicCloudDatabase
let query = CKQuery(recordType: "Inventory", predicate: NSPredicate(format: "TRUEPREDICATE", argumentArray: nil))
publicData.performQuery(query, inZoneWithID: nil){results, error in
if error == nil {
for res in results {
let record: CKRecord = res as! CKRecord
if(record.objectForKey(("TrackerModel")) as! String == "Link 300"){
self.count300 = self.count300++
}else if(record.objectForKey(("TrackerModel")) as! String == "Link 410"){
self.count410 = self.count410++
}else if(record.objectForKey(("TrackerModel")) as! String == "Link 510"){
self.count510 = self.count510++
}
}
}else{
println(error)
}
}
self.detailArray.addObject(self.count300.description)
self.detailArray.addObject(self.count410.description)
self.detailArray.addObject(self.count510.description)
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.textArray.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) ->UITableViewCell {
var cell: UITableViewCell = self.tableView.dequeueReusableCellWithIdentifier("cell") as! UITableViewCell
cell.textLabel?.text = self.textArray.objectAtIndex(indexPath.row) as? String
cell.detailTextLabel?.text = self.detailArray.objectAtIndex(indexPath.row) as? String
return cell
}
Many thanks - Robbie
The closure associated with the performQuery will complete asynchronously - that is after viewDidLoad has finished. You need to make sure that you reload your table view once the query has completed and you have the data. You also have a problem because you are updating your totals outside the closure - this code will also execute before the data has loaded.
Finally, make sure that any update to the UI (such as reloading the table view) is dispatched on the main queue
override func viewDidLoad() {
super.viewDidLoad()
self.navigationItem.hidesBackButton = true;
self.textArray.addObject("Link 300")
self.textArray.addObject("Link 410")
self.textArray.addObject("Link 510")
let container = CKContainer.defaultContainer()
let publicData = container.publicCloudDatabase
let query = CKQuery(recordType: "Inventory", predicate: NSPredicate(format: "TRUEPREDICATE", argumentArray: nil))
publicData.performQuery(query, inZoneWithID: nil){results, error in
if error == nil {
for res in results {
let record: CKRecord = res as! CKRecord
if(record.objectForKey(("TrackerModel")) as! String == "Link 300"){
self.count300++
}else if(record.objectForKey(("TrackerModel")) as! String == "Link 410"){
self.count410++
}else if(record.objectForKey(("TrackerModel")) as! String == "Link 510"){
self.count510++
}
}
self.detailArray.addObject(self.count300.description)
self.detailArray.addObject(self.count410.description)
self.detailArray.addObject(self.count510.description)
dispatch_async(dispatch_get_main_queue(), { () -> Void in
self.tableView.reloadData()
})
}else{
println(error)
}
}
}
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.