Array index out of range on refresh - ios

I have a Posts class that I am querying in a UITableViewController using Parse as my backend.
In my viewDidAppear I call my loadData() function. And then I have a var refresher = UIRefreshControl() that is responsible for my refresh() function.
After a few time reloading the data I get a a fatal error: Array index out of range and the following line of code highlighted which is in my cellForRowAtIndexPath.
What was interesting is when I printed out what was in my
index path using println(drive), all the posts were there. But then there were instances where some posts appeared twice then the app would crash.
timelineData.removeAll(keepCapacity: false)
I thought that having this should clear everything so I am not sure why this is happening.
Here is my code for my refresh function.
func refresh()
{
println("refresh table from pull")
timelineData.removeAll(keepCapacity: false)
var findItemData:PFQuery = PFQuery(className:"Posts")
findItemData.addDescendingOrder("createdAt")
findItemData.findObjectsInBackgroundWithBlock{
(objects:[AnyObject]? , error:NSError?) -> Void in
if error == nil
{
self.timelineData = objects as! [PFObject]
self.newsFeedTableView.reloadData()
}
}
self.refresher.endRefreshing()
}
I tried using Parse's query.cachePolicy but that didn't matter because the crash kept happening. https://parse.com/docs/ios/guide#queries-querying-the-local-datastore
I also thought it was because I have Parse.enableLocalDatastore() but still no luck.
I do call my other function loadData in my viewDidAppear as mentioned earlier, not sure if this might be the problem, but I don't know how to check for data when there is an update. Still not sure if this is the source of the problem.
EDIT 1
I have attached my timelinedata count in several functions. Second image is when I print the count in my cellForRowIndexPath

Try to:
findItemData.findObjectsInBackgroundWithBlock{
(objects:[AnyObject]? , error:NSError?) -> Void in
if error == nil
{
timelineData.removeAll(keepCapacity: false)
self.timelineData = objects as! [PFObject]
self.newsFeedTableView.reloadData()
}
}
It could happen that you have inconsistent data before you actually populate your list. This way you will have your data if some kind of error occurs, so you are safe from that point as well.

Related

Objective C: Unable to execute callback method in viewDidLoad function

I am new to iOS objective C and currently am enhancing some features to an existing project. I would like to implement a callback method to return the results from an API call in the viewDidLoad method as I need the results to determine the number of icons to display on the screen.
I have looked around for various answers, however, I am not sure what has went wrong and I am unable to return the responses from the APIs to the NSMutableArray in the ViewDidLoad method. I would greatly appreciate any help on how to go about get the response into the customerArray in the viewDidLoad method
This is the Service Delegate header file that defines the method:
I made use of the first method requestCompleted in my ViewController file to retrieve the response returned from the API. Within this method, customerArray which is a globally declared array contains the items that was returned from the API, however when this array was called in the viewDidLoad method, it appears to be null.
I tried the following way to retrieve using the dispatch async method, but the customer Array is still empty
You are printing customerArray, before dispatch_async executed. Try putting breakpoint in requestCompleted method or put print statement in requestCompleted method. and check the value.
You can't execute the callback method in viewDidLoad function. And then this attempt is not good pattern in iOS.
I understand what you want. You want to display various icons when view controller is appeared.
For this, you may need to use UITableView or CollectionView.
In this case, i have been doing following.
First, Run API call by using Alamofire
Second. Show loading icon (by using HUD)
Third, In API callback, fetch the list information and reload tableview.
*Following is swift code but it is very similar with Objective C.
override func viewDidLoad() {
super.viewDidLoad()
HUD.show(.rotatingImage(UIImage(named: "rotateLogo")), onView: nil)
APIManager.request(Constant.sdkCredential,
parameters: [:],
fullResponse:true)
{ (response, success, error) in
if success == true {
guard (response as? Dictionary<String,Any>) != nil else {
self.showAlert(withTitle: "Error", message: "Invalid Credential")
return
}
if let dict = response as? Dictionary<String,Any> {
print(dict)
if let dictData = dict["data"] as? Dictionary<String,Any>{
self.tableView.reloadData();
}
}
} else {
HUD.hide()
self.showAlertWithError(error)
}
}
}

How to Update uitableview from CloudKit using discoverAllIdentities

So, I am new to cloudKit and to working with multiple threads in general, which I think is the source of the problem here, so if I simply need to research more, please just comment so and I will take that to heart.
Here is my question:
I am working in Swift 3 Xcode 8.1
I have in my view controller this variable:
var contactsNearby: [String:CLLocation]?
Then at the end of ViewDidLoad I call one of my view controllers methods let's call it:
populateContactsNearby()
inside that method I call:
container.discoverAllIdentities(completionHandler: { (identities, error) in
for userIdentity in identities! {
self.container.publicCloudDatabase.fetch(withRecordID: userIdentity.userRecordID!, completionHandler: { (userRecord, error) in
let contactsLocation = userRecord?.object(forKey: "currentLocation")
if self.closeEnough(self.myLocation!, contactLocation: contactsLocation as! CLLocation) {
var contactsName = ""
contactsFirstName = userIdentity.nameComponents?.givenName
if contactsName != "" && contactsLocation != nil {
self.contactsNearby?["\(contactsName)"] = contactsLocation as? CLLocation
}
}
})
}
})
}
I apologize if I am missing or have an extra bracket somewhere. I have omitted some error checking code and so forth in order to get this down to bare-bones. So the goal of all that is to populate my contactsNearby Dictionary with data from CloudKit. A name as the key a location as the value. I want to use that data to populate a tableview. In the above code, the call to closeEnough is a call to another one of my view controllers methods to check if the contact from CloudKit has a location close enough to my user to be relevant to the apps purposes. Also myLocation is a variable that is populated before the segue. It holds the CLLocation of the app users current location.
The Problem:
The if statement:
if contactsName != "" && contactsLocation != nil { }
Appears to succeed. But my view controllers variable:
var contactsNearby: [String:CLLocation]?
Is never populated and I know there is data available in cloudKit.
If it's relevant here is some test code that I have in cellForRowAtIndexPath right now:
let contact = self.contactsNearby?.popFirst()
let name = contact?.key
if name != nil {
cell.textLabel?.text = name
}else {
cell.textLabel?.text = "nothing was there"
}
My rows alway populate with "nothing was there". I have seen answers where people have done CKQueries to update the UI, but in those answers, the user built the query themselves. That seems different from using a CloudKit function like discoverAllIdentities.
I have tried to be as specific as possible in asking this question. If this question could be improved please let me know. I think it's a question that could benefit the community.
Okay, I need to do some more testing, but I think I got it working. Thank you Paulw11 for your comment. It got me on the right track.
As it turns out there were 2 problems.
First, as pointed out I have an asynchronous call inside a for loop. As recommended, I used a dispatchGroup.
Inside the cloudKit call to discoverAllIdentities I declared a dispatchGroup, kind of like so:
var icloudDispatchGroup = DispatchGroup()
Then just inside the for loop that is going to make an async call, I enter the dispatchGroup:
icloudDispatchGroup.enter()
Then just before the end of the publicCloudDatabase.fetch completion handler I call:
icloudDispatchGroup.leave()
and
icloudDispatchGroup.wait()
Which, I believe, I'm still new to this remember, ends the dispatchGroup and causes the current thread to wait until that dispatchGroup finishes before allowing the current thread to continue.
The Above took care of the multithreading issue, but my contactsNearby[String:CLLocation]? Dictionary was still not being populated.
Which leads me to the 2nd problem
At the top of my view controller I declared my Dictionary:
var contactsNearby: [String: CLLocation]?
This declared a dictionary, but does not initialize it, which I don't think I fully realized, so when I attempted to populate it:
self.contactsNearby?["\(contactsName)"] = contactsLocation as? CLLocation
It quietly failed because it is optional and returned nil
So, in viewDidLoad before I even call populateContactsNearby I initialize the dictionary:
contactsNearby = [String:CLLocation]()
This does not make it cease to be an optional, which Swift being strongly typed would not allow, but merely initializes contactsNearby as an optional empty Dictionary.
At least, that is my understanding of what is going on. If anyone has a more elegant solution, I am always trying to improve.
In case you are wondering how I then update the UI, I do so with a property observer on the contactsNearby Dictionary. So the declaration of the dictionary at the top of the view controller looks like this:
var contactsNearby: [String: CLLocation]? {
didSet {
if (contactsNearby?.isEmpty)! || contactsNearby == nil {
return
}else{
DispatchQueue.main.sync {
self.nearbyTableView.reloadData()
}
}
}
}
I suppose I didn't really need to check for empty and nil. So then in cellForRowAtIndexPath I have something kind of like so:
let cell = tableview.dequeueReusableCell(withIdentifier: "nearbyCell", for: indexPath)
if contactsNearby?.isEmpty == false {
let contact = contactsNearby?.popFirst()
cell.textLabel?.text = contact?.key
}else {
cell.textLabel?.text = "Some Placeholder Text Here"
}
return cell
If anyone sees an error in my thinking or sees any of this heading for disaster, feel free to let me know. I still have a lot of testing to do, but I wanted to get back here and let you know what I have found.

Handle large JSON files in TableView

I'm trying to parse a large JSON file (approx: 1000 rows containg a tuple with 8 strings) and display this in a UITableView. What I already have is working but I am looking for a more efficient way of displaying them.
At the moment my code looks likes this:
public func GET(request: String, callback: (result: JSON?, response: NSHTTPURLResponse?, error: NSError?) -> Void) {
let session = NSURLSession.sharedSession()
let url = NSURL(string : "SOMEURL")
let task = session.dataTaskWithURL(url!){
(data, response, error) -> Void in
if error != nil {
callback(result: nil, response: response as? NSHTTPURLResponse, error: error!)
} else {
callback(result: JSON(data : data!), response: response as? NSHTTPURLResponse, error: nil)
}
}
task.resume()
}
This does parse the data using SwiftJSON (see JSON(data : data!)), then when it comes to actually filling an array i use a class containing two attributes (one for the Main text in table and one for detail text)
class SomeClass {
let MainText : String
let DetailText : String
init(MainText : String, DetailText : String) {
self.MainText = MainText
self.DetailText = Detailtext
}
}
Now in the UITableView i have a .swift file and in the
override func ViewDidLoad() {
//code
}
I use a loop to get the data from the result callback in the GET method to append to an array of
var rows : [SomeClass] = []
This is very CPU intensive but I did not find another way to deal with this problem. I tried only displaying 50 rows in the table and only creating 50 class items for the rows. But none of that matters, what I fear is that the SwiftyJSON way of dealing with this problem is not the right one but i thought that maybe I am overlooking something.
If I understood your problem, you are worried about CPU / Energy Efficiency.
What you should consider, if it's not how your app already works, is implementing the parsing process in the background thread, make your [SomeClass] array observable and update the table when it changes (aka when the background parsing added an new value to it).
So first make your parsing function run in background (for instance with the Async GCD wrapper) :
func callback(JSON?, response: NSHTTPURLResponse, error: NSError?) {
Async.background {
//Do your JSON parsing stuff here, XXX is a SomeClass object
rows <- rows + [XXX]
}
}
You might have noticed the unusual syntax for the array appending method. That's because making your array "observable" is part of the solution. I advise you to get the Observable-Swift library to make it easier to observe.
Once added to your project, change your array declaration :
var rows = Observable([SomeClass]())
Now implement the method that will be called when your callback parsed a new item (for instance in your viewDidLoad:)
rows.afterChange += { self.table.reloadData() }
where table is your table view
If you want to implement a power-friendly runtime, you might want to update the table every time 50 or 100 objects are added to the array. This can be done so (if you want to do so do not implement the method right above):
rows.afterChange += { if $1.count / 100 = 1 { self.table.reloadData() }}
where 100 is the value of new object required to be added in order to update the table. With Observable-Swift, $0 represents the array before it was updated and $1 the array after its update.
One last thing : the rows array is no longer of type [SomeClass] but Observable<SomeClass>. If you want to access the [SomeClass] value, just replace rows by rows.value
Hope I didn't misunderstood your question. Anyway if I did, I think that can still help providing a better implementation of JSON parsing.
You should not be worried about how much of data you have to display in TableView.
TableView class handles everything for you as long as you pass the json object properly as a Tablesource.
It's actually a pretty good concern about how you use the resources. Normally, we will go with pagination if you don't want to query back whole amount of data from a request. Then, you will implement some proper logic based on the skip and limit in order to get further data.
As for the UITableView, there is nothing to worry about. Because, it's developed in an efficient way. The total number of cell in memory is the total number of cell visible. The UITableView will help populating the data via delegation methods. It's not like: you have 500 rows of data, then it has 500 UITableViewCell. It's reusability.

Query class in Parse (swift)

I have a strange issue. The following block of code is placed in my viewDidAppear section of the first View Controller, and when it runs, println(latArray), println(longArray), println(addressArray), all return no value. In the console it returns [] for all three arrays. HOWEVER, when I go to another view controller, and go back, it will populate with the data from Parse. Why is this? Why wont latArray, longArray, and addressArray populate when the app is loaded the first time with the viewWillAppear method?
var query = PFQuery(className: "NewLog")
// Add a where clause if there is a search criteria
query.whereKey("Type", containsString: newRecordCreated)
println(query)
query.findObjectsInBackgroundWithBlock({
(objects, error) -> Void in
if error == nil {
// Results were successfully found
if let objects = objects as? [PFObject] {
for object in objects {
//println(object["follower"])
latArray.append(object["Lat"] as! Double)
longArray.append(object["Long"] as! Double)
addressArray.append(object["Address"] as! String)
}
}
// self.tableView.reloadData()
} else {
println("error")
// The network was inaccessible and we have no cached data for
// this query.
}
})
println(latArray)
println("^^Latitude values")
println(longArray)
println("^^Longitude values")
println(addressArray)
println("^^Address values")
}
The query is being executed in the background. The code in the block (closure) is executed when the query is finished. Note: this might be and most likely is after viewDidAppear finishes. If you put your print statements after your for loop, you should see values. If you uncomment your reloadData method, the table should be updated with new information when the query finishes.

Swift Variables Scope Issue

I declare arr1 and arr2 as arrays globally in my viewcontroller.
In the following function, I add to them in a parse query. Xcode gets buggy when I don't include "self" before arr1 inside the query, so I included it. Outside the query, in the space where I have marked below, I try to access arr1 and arr2. But regardless of whether I try self.arr1 or arr1, the array turns out to be empty at that point. I think this is some sort of scope issue with swift, and I've had quite a bit of trouble with it so any help would be much appreciated.
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if (segue.identifier == "vF") {
var destVC = segue.destinationViewController as vF
destVC.namePassed = fV
var query = PFQuery(className:"class")
query.whereKey("createdBy", equalTo:fV)
query.findObjectsInBackgroundWithBlock {
(objects: [AnyObject]!, error: NSError!) -> Void in
if error == nil {
// The find succeeded.
NSLog("Successfully retrieved \(objects.count) records.")
// Do something with the found objects
for object in objects {
self.arr1.append(object["field1"]! as Int)
self.arr2.append(object["field2"]! as String)
}
} else {
// Log details of the failure
}
}
// I want to access arr1 and arr2 right here, but when I do they come up empty
// I have tried accessing with self.arr1 and arr1 but neither works
}
}
The findObjectsInBackgroundWithBlock is async so it will happen in the background thread while your program still running in the main thread, by the point your background thread comeback with the data you need you program is why passed the point you indicate.
One of the best solutions for your problem is to add observers for the arr1 and arr2 so you will be notified when it happens. Add observers to array can be a little bit trick and I want to keep it simple for you, so I would recommend you to create a boolean variable that tells you when the value finish change. To do so you will need to create the variable like this
var arrayDidChange:Bool = false {
didSet{
if arrayDidChange{
callFuncDataReadArrayHere()
}
}
}
Now everytime you change the values (add or edit) in the array you set arrayDidChange to true and in the end of callFuncDataReadArrayHere after do all you need to do you set arrayDidChange to false.
This way you will always be sure you will access the values in the array after it be populate.
I hope that helped you!
You should connect the segue directly from the controller instead of the cell. Implement didSelectRowAtIndexPath, and put the query there, and call performSegueWithIdentifier:sender: inside the completion block of the asynchronous method. Now, when you implement prepareForSegue, the arrays will be available to pass to the destination view controller.
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
var query = PFQuery(className:"class")
query.whereKey("createdBy", equalTo:fV)
query.findObjectsInBackgroundWithBlock {
(objects: [AnyObject]!, error: NSError!) -> Void in
if error == nil {
// The find succeeded.
NSLog("Successfully retrieved \(objects.count) records.")
// Do something with the found objects
for object in objects {
self.arr1.append(object["field1"]! as Int)
self.arr2.append(object["field2"]! as String)
}
self.performSegueWithIdentifier("vF", sender: self)
} else {
// Log details of the failure
}
}
}
There will be some delay before the segue actually proceeds with this approach, because it won't happen until findObjectsInBackgroundWithBlock returns its results. If that's not acceptable, then you could still pass the arrays in prepareForSegue (in the same place I show after the "for object in objects" clause), but then the arrays will not be immediately available in the destination view controller, and you will have to deal with that.

Resources