tableview's Datasource array's value is automatically changes to 0 - ios

var noteList:NSMutableArray = NSMutableArray() // declaration of `noteObjects`
query.findObjectsInBackgroundWithBlock { ( objects, error) -> Void in
if (error == nil) {
let temp: NSArray = objects as NSArray!
self.noteList = temp.mutableCopy() as! NSMutableArray
self.tableView.reloadData()
}else {
print(error!.userInfo)
}
}
//populating noteObjects
i have a tableView who's datasource is an Array 'noteObjects' its type is NSMutableArray , so the problem is that every time i open my tableView my 'noteObjects' array's Value is 0 but then it automatically changes to desired value , how can i say this ? i did this in different stages of my tableViewController -
i printed the noteObjects.count in ViewDidload
override func viewDidLoad() {
super.viewDidLoad()
print("\(noteObjects.count) viewDidlaod3") }
output :
0 viewDidlaod3
inside cellForRowAtindexPath i printed this
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
print("\(noteObjects.count) in cellForAtIndexPath")
return cell }
output :
18 in cellForAtIndexPath
and if i move to another View then again open this tableView it keeps giving the sae result , (note object.count = 0 at first)
i want to use that noteObjects.count so that i can confirm that if the tableView's datasource is empty so i can show a message , but when am using this then its always showing that my tableview is empty because at first noteObjects.count is 0
if details provided above is not enough then please let me know i'll fix it

The api call 'findeObjectsInBackground' is asynchronous meaning the closure is executed later in time when result is obtained in a different thread. So getting back to the main thread and reloading the table view when data is ready will solve the problem. You can read more about these type of closures as they are very common in iOS.
query.findObjectsInBackgroundWithBlock { ( objects, error) -> Void in
if (error == nil) {
let temp: NSArray = objects as NSArray!
dispatch_async(dispatch_get_main_queue(), {
self.noteList = temp.mutableCopy() as! NSMutableArray
self.tableView.reloadData()
})
}
else {
print(error!.userInfo)
}
}

use a if statement after the synchronisation method
if noteObjects.count < 1 {
let Alert = UIAlertView(title: "No Data to Display", message: "ther's nothing bro you should pull to refresh", delegate: self, cancelButtonTitle: "OKAY Got IT")
Alert.show()
}

just moved my code from viewDidLoad to viewDidAppear and it worked For me
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
self.fetchAllObjectsFromLocalDataStore()
print("\(noteObjects.count) view appears")
if noteObjects.count < 1 {
let Alert = UIAlertView(title: "No Data to Display", message: "ther's nothing bro you should pull to refresh", delegate: self, cancelButtonTitle: "OKAY Got IT")
Alert.show()
}
}
now noteObject.count is getting check after updating its Value

Related

How to reload tableview after adding new entry?

I am creating a cloudkit tableview. I load the app and my tableview appears with my entries from cloud kit.
I then use my add method insertNewObject which adds the record to cloud kit but this does not show up in my tableview. It will only show up on my next run of the app.
func insertNewObject(sender: AnyObject) {
let record = CKRecord(recordType: "CloudNote")
record.setObject("New Note", forKey: "Notes")
MyClipManager.SaveMethod(Database!, myRecord:record)
dispatch_async(dispatch_get_main_queue()) {
self.tableView.reloadData()
}
}
This is my add method. I am calling tableview reload as you can see but nothing is happening.
My tableview creation code:
// Tableview stuff --- Done
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
/////// Get number of rows
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return objects.count
}
//// FIll the table
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath)
let object = objects[indexPath.row]
cell.textLabel!.text = object.objectForKey("Notes") as? String
return cell
}
override func tableView(tableView: UITableView, canEditRowAtIndexPath indexPath: NSIndexPath) -> Bool {
// Return false if you do not want the specified item to be editable.
return true
}
As requested: Method that saves to CloudDB
func SaveMethod(publicDatabase: CKDatabase, myRecord: CKRecord ) -> CKRecord {
publicDatabase.saveRecord(myRecord, completionHandler:
({returnRecord, error in
if let err = error {
self.notifyUser("Save Error", message:
err.localizedDescription)
} else {
dispatch_async(dispatch_get_main_queue()) {
self.notifyUser("Success",
message: "Record saved successfully")
}
}
}))
return myRecord
}
My viewdidload method in masterview:
override func viewDidLoad() {
super.viewDidLoad()
// Database loading on runtime
Database = container.privateCloudDatabase
///Build Query
let query = CKQuery(recordType: "CloudNote", predicate: NSPredicate(format: "TRUEPREDICATE"))
///Perform query on DB
Database!.performQuery(query, inZoneWithID: nil) { (records, error) -> Void in
if (error != nil) {
NSLog("Error performing query. \(error.debugDescription)")
return
}
self.objects = records!
dispatch_async(dispatch_get_main_queue()) {
self.tableView.reloadData()
}
}
You should not reload your entire tableView when you insert a single object. Only do that when you know ALL the data has changed.
To do what you want, this is the order:
Insert a new data object into your datasource (self.objects). Make sure you get the index of where it ends up in the array.
Call insertRowAtIndexPath: with the correct indexPath on your tableView. This will make sure your data and tableView are in sync again, and tableView:cellForRowAtIndexPath: is called for at least your new data object (and possible others, as certain cells might now be reused to display other data).
Note that the order is always: update your data first, then update your UI (the only place I know of that his is hairy is when using a UISwitch).

Swift 2 + Parse: Array index out of range

SOMETIMES THE REFRESH WORKS SOMETIMES IT DOESN'T
I have a UITableViewController which is basically a news feed. I have also implemented a pull to refresh feature. However sometimes when I pull to refresh it gives me the error
'Array index out of range'.
I know this means an item it is trying to get does not exist but can you tell me why? Here is my code:
override func viewDidLoad() {
super.viewDidLoad()
refresher = UIRefreshControl()
refresher.attributedTitle = NSAttributedString(string: "Pull to refresh")
refresher.addTarget(self, action: "refresh", forControlEvents: UIControlEvents.ValueChanged)
self.tableView.addSubview(refresher)
refresh()
tableView.delegate = self
tableView.dataSource = self
}
and the refresh() function:
func refresh() {
//disable app while it does stuff
UIApplication.sharedApplication().beginIgnoringInteractionEvents()
//get username and match with userId
let getUser = PFUser.query()
getUser?.findObjectsInBackgroundWithBlock({ (objects, error) -> Void in
if let users = objects {
//clean arrays and dictionaries so we dont get indexing error???
self.messages.removeAll(keepCapacity: true)
self.users.removeAll(keepCapacity: true)
self.usernames.removeAll(keepCapacity: true)
for object in users {
if let user = object as? PFUser {
//make userId = username
self.users[user.objectId!] = user.username!
}
}
}
})
let getPost = PFQuery(className: "Posts")
getPost.findObjectsInBackgroundWithBlock { (objects, error) -> Void in
if error == nil {
if let objects = objects {
self.messages.removeAll(keepCapacity: true)
self.usernames.removeAll(keepCapacity: true)
for object in objects {
self.messages.append(object["message"] as! String)
self.usernames.append(self.users[object["userId"] as! String]!)
self.tableView.reloadData()
}
}
}
}
self.refresher.endRefreshing()
UIApplication.sharedApplication().endIgnoringInteractionEvents()
}
and:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let myCell = tableView.dequeueReusableCellWithIdentifier("SinglePostCell", forIndexPath: indexPath) as! PostCell
//ERROR GETS REPORTED ON THE LINE BELOW
myCell.usernamePosted.text = usernames[indexPath.row]
myCell.messagePosted.text = messages[indexPath.row]
return myCell
}
You have a race condition given you are doing two background tasks, where the second depends on values returned from the first. getUser?.findObjectsInBackgroundWithBlockwill return immediately, and getPost.findObjectsInBackgroundWithBlock will start executing. The getPost should be inside the block for getUser, to ensure the sequence is correct.
Similarly, the following two lines should be inside the second block:
self.refresher.endRefreshing()
UIApplication.sharedApplication().endIgnoringInteractionEvents()
Given the error line, you probably also have a race condition between the two background tasks and displaying the tableView. I would be inclined to try:
func tableView(tableView:UITableView!, numberOfRowsInSection section:Int) {
return self.refresher.refreshing ? 0 : self.usernames.count
}
This way you won't touch self.usernames until the background refresh is finished (as long as you remember to put endRefreshing inside the second block, which is also put inside the first block).
I Believe that in self.users[user.objectId!] = user.username! the user.ObjectId is some random value assigned by parse which looks like this: "34xcf4". This is why you might be getting 'Array index out of range'.
There are two required methods for configuring a UITableView:
tableView(_:cellForRowAtIndexPath:)
and
tableView(_:numberOfRowsInSection:)
In your code you are presenting only one required method, if you don't implement the second method then it that may cause errors.
Check the documentation at:
https://developer.apple.com/library/ios/documentation/UIKit/Reference/UITableViewDataSource_Protocol/#//apple_ref/occ/intfm/UITableViewDataSource/tableView:cellForRowAtIndexPath:
You are calling self.tableView.reloadData() on every addition to your array and doing so in a background thread.
As a general rule, you should not do UI updates in a background thread. When you clear self.messages and self.usernames, because you are in background thread, nothing prevents the tableview from trying to get a cell at an index that no longer has any data in the array.
If you want to keep your code in the background thread (risky as it may be), you should at least call .beginUpdates before reloading your arrays and wait until they're all done before calling reload and endUpdates.

Building table view while getting number of rows from completion block

I am trying to retrieve the calendar events in swift 2, and I can not solve this: to build the table view I need to know the number of cells, which I can get from a method like this (for the sake of simplicity array is String):
func fetchCalendarEvents () -> [String] {
var arrayW = [String]()
let eventStore : EKEventStore = EKEventStore()
eventStore.requestAccessToEntityType(EKEntityType.Event, completion: {
granted, error in
if (granted) && (error == nil) {
print("access granted: \(granted)")
//do stuff...
}
else {
print("error: access not granted \(error)")
}
})
return arrayW
}
I am calling this method in viewDidLoad, and save the array to var eventArray. Immediately the following method gets called:
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return eventArray.count
}
The problem is that the completion block of the fetchCalendarEvents is not complete at this point, and therefore returns 0 (while there are events in the calendar).
My question: how can I handle building the table view from array that I get from method, that has completion block, and takes some time to be completed?
Add aBlockSelf.tableView.reloadData() in your calendar completion block to reload your table with fetched data.
Here aBlockSelf is a weak reference to self because its being passed to a block.
EDIT: Post OP comment - Try this:
weak var aBlockSelf = self
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if (eventArray.count == 0) { //or unwrap value, depends on your code
return 0 // or 1, if you want add a 'Loading' cell
} else {
return eventArray.count
}
}
And, when you get eventArray, just reload table with
//without animation
tableView.reloadData()
//or with, but it can get little tricky
tableView.insertRowsAtIndexPaths(indexArray, withRowAnimation:UITableViewRowAnimationRight];
Make 2 cells type. One for events and one which just say "Loading...". While your block in progress show only 1 cell with "Loading...". When an events will retrieve hide this cell and reload table with events.
Cheers.
Ok, all seemed to answer relatively correct, but not exactly in the way that worked straight. What worked for me, is that instead of returning array, I just need to reload the table view after the for loop, where array is built up:
func fetchCalendarEvents () {
let eventStore : EKEventStore = EKEventStore()
eventStore.requestAccessToEntityType(EKEntityType.Event, completion: {
granted, error in
if (granted) && (error == nil) {
print("access granted: \(granted)")
let startDate=NSDate().dateByAddingTimeInterval(-60*60*24)
let endDate=NSDate().dateByAddingTimeInterval(60*60*24*3)
let predicate2 = eventStore.predicateForEventsWithStartDate(startDate, endDate: endDate, calendars: nil)
print("startDate:\(startDate) endDate:\(endDate)")
let events = eventStore.eventsMatchingPredicate(predicate2) as [EKEvent]!
if events != nil {
var arrayOfEvents = [CalendarEventObject]()
for event in events {
var eventObject: CalendarEventObject
// (ಠ_ಠ) HARDCODEDE
eventObject = CalendarEventObject(id: 0, title: event.title, location: event.location!, notes: event.notes!, startTime: event.startDate, endTime: event.endDate, host: "host", origin: "origin", numbers: ["0611111111", "0611111112"], passcodes: ["123456", "123457"], hostcodes: ["123458", "123459"], selectedNumber: "0611111113", selectedPasscode: "123457", selectedHostcode: "123459", scheduled: true, parsed: false, update: true, preferences: [], eventUrl: "www.youtube.com", attendees: [])
arrayOfEvents.append(eventObject)
}
self.eventArray = arrayOfEvents
//reload data after getting the array
dispatch_async(dispatch_get_main_queue(), { () -> Void in
self.tableView.reloadData()
})
}
}
else {
print("error: access not granted \(error)")
}
})
}

How to synchronize a parse-read image with screen display in iOS

In TableViewController I display a table of records read from a parse database. Each cell displays a thumbnail, a title and a note. When a row is selected, the row index is saved and a segue is initiated to a detailView Controller which displays the image saved in a global UIImage variable (noteImage). In prepareForSegue I invoke loadImageFromParse in background which reads the image associated with the thumbnail and saves it into noteImage.
My problem is that there is a slight delay for the image to be read from parse so the detailViewController displays a blank. When I back up from the detailViewController to the TableViewController and reselect the row the picture is displayed.
I need a way to hold the segue invocation till the image has been read from Parse.
Any help would be appreciated.
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
selectedRow = indexPath.row
self.performSegueWithIdentifier("NoteSegue", sender: self)
tableView.deselectRowAtIndexPath(indexPath, animated: true)
}
// MARK: - Navigation
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "NoteSegue"{
let vc = segue.destinationViewController as NoteViewController
vc.selectedRow = selectedRow
self.loadImageFromParse(objectIds[selectedRow])
}
}
// Load one image from parse
func loadImageFromParse (objectId: String) {
var query = PFQuery(className:"Record")
query.whereKey("objectId", equalTo:objectId)
query.findObjectsInBackgroundWithBlock {
(objects: [AnyObject]!, error: NSError!) -> Void in
if error == nil {
println("Successfully retrieved \(objects.count) records.")
for object in objects {
let userImageFile = object["image"] as PFFile!
userImageFile.getDataInBackgroundWithBlock {
(imageData: NSData!, error: NSError!) -> Void in
if error == nil {
noteImage = UIImage(data:imageData)!
println("Image successfully retrieved")
}
}
}
} else {
NSLog("Error: %# %#", error, error.userInfo!)
}
}
}
You said:
I need a way to hold the segue invocation till the image has been read from Parse.
That is, as I think you now guessed, not really the best way to do it. It's much better to initiate the transition to the next scene, and if the image is not available, present a UIActivityIndicatorView (a spinner), retrieve the image, and when it's done, remove the spinner and show the image. But delaying for a fixed amount of time is not going to work well: Sometimes you'll make the user wait longer than needed; sometimes you won't have waited long enough. (And, of course, doing the request synchronously is invariably not the right approach.)
The implementation of this is probably simplified if you put this image retrieval code inside the destination view controller. To have one view controller initiate an asynchronous process and have another be notified of the completion is awkward (though can be done through judicious use of notifications). But in this example (where you don't know which image to retrieve until you select a row), I see little benefit.
Having said all of that, the code is initiating two asynchronous requests, one for Record and one for the image itself. I wonder if you could capture this Record information when you populate this initiate table view. That way you only have to retrieve the image. Now perhaps Parse does some caching and the retrieval of the Record is incredibly fast anyway (so benchmark it before you refactor the code), but if not, you might want to capture the PFFile objects when you populate the original table view, saving you from having to do two queries.
I resolved the problem by introducing a 1 second delay after I start the background method loadImageFromParse and before I initiate a segue in performSegueWithDelay. This gives a slight pause after I select the row and before the image is visible in the target segue but it ensures that the image is available. Obviously there is a risk that this will not always work depending on size of the file I am getting from Parse.com and also the network load. I am considering putting an alert to the user if the image has not loaded.
Here is the code:
func performSegueWithDelay(timer: NSTimer) {
// action function for timer in didSelectRowAtIndexPath
self.performSegueWithIdentifier("NoteSegue", sender: self)
}
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
selectedRow = indexPath.row
self.loadImageFromParse(objectIds[selectedRow])
let myTimer : NSTimer = NSTimer.scheduledTimerWithTimeInterval(1, target: self,
selector: Selector("performSegueWithDelay:"), userInfo: nil, repeats: false)
tableView.deselectRowAtIndexPath(indexPath, animated: true)
}
// MARK: - Navigation
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "NoteSegue"{
let vc = segue.destinationViewController as NoteViewController
}
}
// Load one image from parse
func loadImageFromParse (objectId: String) {
var query = PFQuery(className:"Record")
query.whereKey("objectId", equalTo:objectId)
query.findObjectsInBackgroundWithBlock {
(objects: [AnyObject]!, error: NSError!) -> Void in
if error == nil {
println("Successfully retrieved \(objects.count) records.")
for object in objects {
let userImageFile = object["image"] as PFFile!
userImageFile.getDataInBackgroundWithBlock {
(imageData: NSData!, error: NSError!) -> Void in
if error == nil {
noteImage = UIImage(data:imageData)!
println("Image successfully retrieved")
}
}
}
} else {
NSLog("Error: %# %#", error, error.userInfo!)
}
}
}

IOS/Swift rendering table after JSON request

I am new to IOS and swift. I am trying to implement an api get request that returns json and then display it in a table. Below is my current code. When I run simulator I am getting the following error:
fatal error: Cannot index empty buffer
If I remove the hardcoded return 3 in the tableView function and instead use doDatItems.count nothing renders in the table because I guess the array of doDatItems starts empty before the get request is made. It seems like a timing thing? How do I ensure the get request is made before the table loads?
import UIKit
class ViewController: UIViewController, UITableViewDelegate {
var doDatItems:[String] = []
#IBOutlet weak var doDatItem: UITextField!
#IBOutlet weak var yourDoDats: UILabel!
#IBAction func addDoDat(sender: AnyObject) {
doDatItems.append(doDatItem.text)
println(doDatItems)
}
override func viewDidLoad() {
super.viewDidLoad()
let urlPath = "http://localhost:3000/dodats"
let url: NSURL = NSURL(string: urlPath)!
let session = NSURLSession.sharedSession()
let task = session.dataTaskWithURL(url, completionHandler: {data, response, error -> Void in
println("Task completed")
if((error) != nil) {
// If there is an error in the web request, print it to the console
println(error.localizedDescription)
}
var err: NSError?
var jsonResult = NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions.MutableContainers, error: &err) as NSDictionary
if(err != nil) {
// If there is an error parsing JSON, print it to the console
println("JSON Error \(err!.localizedDescription)")
} else {
let dataArray = jsonResult["dodats"] as [String]
for item in dataArray {
self.doDatItems.append(item)
}
// println(self.doDatItems)
}
})
task.resume()
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 3
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
{
let cell = UITableViewCell(style: UITableViewCellStyle.Default, reuseIdentifier: "Cell")
println(self.doDatItems)
cell.textLabel?.text = self.doDatItems[indexPath.row]
return cell
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
I found several problems -
You ViewController should conform to UITableViewDatasource (which is missing, not sure how it went that far)
Do not return 3 when self.doDatItems does not have any items. It will cause a crash. As long as the data loads let the table remain empty. return self.doDatItems.count
Once data is loaded and ready to display from self.doDatItems array, just call reloadData() method of your table view.
Before that, you should have a reference (or IBOutlet) of your tableView so that you can call reloadData() from anywhere.
You need to trigger a page refresh once the data has been received and parsed.
Something along the lines of
self.tableView.reloadData()
In this delegate method, which return number of rows,
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int
Don't use static number as 3. Get your Array Count & return the count in here.
After the json response comes, reload the tableView. In objective-c it's done by
[tableView reloadData];

Resources