I have a situation where my table is loading before getting the data from my webservice.
When I debug it, I can see that my NSMutableArray has 123 objects, but it's not being reflected in the table.
This is my connectionDidFinishLoading method:
-(void)connectionDidFinishLoading:(NSURLConnection *)conn{
myData = [[NSString alloc] initWithData:xData encoding:NSUTF8StringEncoding];
SBJsonParser *jParser = [SBJsonParser new];
NSError *error = nil;
NSArray *jsonObjects = [jParser objectWithString:myData error:&error];
NSMutableArray *books = [[NSMutableArray alloc] init];
for(NSDictionary *dict in jsonObjects)
{
Book *b = [Book new];
b.bookName =[dict objectForKey:#"name"];
b.published = [dict objectForKey:#"published"];
[books addObject:b];
}
self.bookArray = books;
[self.tableView reloadData];
NSLog(#"myData = %#",myData);
}
If I debug this I'm getting my jsonObjects and populating my collection. But I notice my table is still empty.
Just to clarify this is my tableView method:
(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
...
Book *b = (Book *)[self.bookArray objectAtIndex:[indexPath row]];
cell.textLabel.text = b.bookName;
....
Can anyone help me with what I'm missing?
You need to consider what is returned by numberOfRowsInSection (and numberOfSections) for the case where there is no data to show and for the case when there is data to show. Every time you call reloadData, the tableView will call these two methods to get the latest information about the data source. If your data has not loaded, then these delegate methods should report appropriate values. If the data is not loaded, you can test for a nil value or do some similar test, and then return appropriate values. Once you data is loaded, the delegate methods should return values based on the size of the contents in the data source.
If you show us your code for these methods, someone can comment on whether this is your problem.
It is because the way to do it is, don't load the table till the contents get downloaded via webservices.
For that don't set the delegates via IB. After the completion of this web request set the delegate and datasource via code and then call reloadData method.Hence we can make the table wait to load till the webcall get processed .Provide some neat activity indicator of custom HUD while data being loaded and that will do the job
Check the calling thread. You cannot have UIKit updates on anything other than mainThread. I think your connectionDidFinishLoading: called on a seperate thread and that is the issue here.
Try
dispatch_async(dispatch_get_main_queue(), ^{
[self.tableView reloadData];
});
Related
I Have a CSV File that contain 6000 rows.
I would like to Populate those 6000 rows in a TableView Then Use a SearchBar to fetch a single row
I used CSVParser but it needs 10 minutes to load the UITableview:
-(void)ViewDidLoad{
CSVParser *parser = [CSVParser sharedInstance];
NSDictionary *dictionary = [parser parseCSVFile: #"ports"];
NSArray* rows = [dictionary allKeys];
}
Then to Populate the tableview:
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
// Return the number of sections.
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
// Return the number of rows in the section.
return 25;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
PortListCell *cell = (PortListCell *)[tableView dequeueReusableCellWithIdentifier:#"Cell"];
cell.Codeport.text = [[rows objectAtIndex:indexPath.row]valueForKey:#"ident"];
cell.Nameport.text = [[rows objectAtIndex:indexPath.row]valueForKey:#"name"];
NSString*CodeCountry = [[[rows objectAtIndex:indexPath.row]valueForKey:#"iso_country"]lowercaseString];
cell.ImageCountry.image = [UIImage imageNamed:[NSString stringWithFormat:#"%#.png",CodeCountry]];
return cell;
}
What is the best way to populate a UITableView with these rows ?
Loading large amounts of data into table views, which will never show the whole amount of data anyway, is commonly done asynchronously, with periodic updates. If you can get into the CSV parser, perhaps it could add rows to the array and call tableview reloadData every 10 rows or something. That way your slow parsing and tableview update would happen in the background, as the table view was already appearing active to the user.
You can use SQLite to import all the data into DB, and use it for more efficiency. I am showing you using FMDB.
The way you are parsing your data, using CSVParser you read the CSV, and insert into DB dynamically.
You can create your insert sql query dynamically using the function below
-(NSDictionary *)createInsertSQLWithValue:(NSDictionary *)value forTable:(NSString *)tableName{
NSString *cols=#"";
NSMutableArray *vals=[[NSMutableArray alloc] init];
NSString *placeHolders=#"";
NSArray *keys=[value allKeys];
for(NSString *key in keys){
[vals addObject:[value objectForKey:key]];
placeHolders=[placeHolders stringByAppendingFormat:#"?,"];
}
//Remove extra ","
if(cols.length>0){
cols=[cols substringToIndex:cols.length-1];
placeHolders=[placeHolders substringToIndex:placeHolders.length-1];
}
NSString *sql=[NSString stringWithFormat:#"insert into %# (%#) values (%#)",tableName,cols,placeHolders];
return #{#"sql":sql,#"args":vals};
}
You just need to do
-(void)populateData{
CSVParser *parser = [CSVParser sharedInstance];
NSArray *ports = [parser parseCSVFile: #"ports"];
for(NSDictionary *value in ports){
[self addPort:value];
}
}
-(void)addPort:(NSDictionary*)value{
//say you created a table ports
NSDictionary *sql=[self createInsertSQLWithValue:value forTable:#"ports"];
BOOL inserted=[self.db executeUpdate:[sql objectForKey:#"sql"] withArgumentsInArray:[sql objectForKey:#"args"]];
if(inserted){
NSLog(#"Port Inserted");
}
}
Note: Make sure you do this when you need to populate the data, if required once, do it once. And too you need to create table with the exact column name as keys in the dictionary too.
Cheers.
I am getting data using json with this code and I need to display it in a tableview with two part code and name the problem is writing it all to an array is taking forever and the array comes back null. How can I get each returned element as its own tableview cell? Hundreds of airports are returned.
NSString* path = #"https://api.flightstats.com/flex/airports/rest/v1/json/active?appId=id&appKey=appkey";
NSMutableURLRequest* _request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:path]];
[_request setHTTPMethod:#"GET"];
NSURLResponse *response = nil;
NSError *error = nil;
NSData* _connectionData = [NSURLConnection sendSynchronousRequest:_request returningResponse:&response error:&error];
if(nil != error)
{
NSLog(#"Error: %#", error);
}
else
{
NSMutableDictionary* json = nil;
if(nil != _connectionData)
{
json = [NSJSONSerialization JSONObjectWithData:_connectionData options:NSJSONReadingMutableContainers error:&error];
}
if (error || !json)
{
NSLog(#"Could not parse loaded json with error:%#", error);
}
else
{
NSMutableDictionary *routeRes;
routeRes = [json objectForKey:#"airports"];
for(NSMutableDictionary *flight in routeRes)
{
NSLog(#"ident is %#", [flight objectForKey:#"name"]);
NSString *code=[json objectForKey:#"fs"];
NSString *name=[flight objectForKey:#"name"];
NSLog(#"code %#, name %#", code, name);
[candyArray addObject:[Candy code:code name:name]];
}
}
_connectionData = nil;
NSLog(#"connection done");
The following is the cellForRowatIndex were nothing is shown
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if ( cell == nil ) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
}
// Create a new Candy Object
Candy *candy = nil;
// Check to see whether the normal table or search results table is being displayed and set the Candy object from the appropriate array
if (tableView == self.searchDisplayController.searchResultsTableView)
{
candy = [filteredCandyArray objectAtIndex:[indexPath row]];
}
else
{
candy = [candyArray objectAtIndex:[indexPath row]];
}
// Configure the cell
[[cell textLabel] setText:[candy name]];
[cell setAccessoryType:UITableViewCellAccessoryDisclosureIndicator];
return cell;
}
This is a sample of what the returned json is
{"airports":[{"fs":"CLO","iata":"CLO","icao":"SKCL","name":"Alfonso B. Aragon Airport","city":"Cali","cityCode":"CLO","countryCode":"CO","countryName":"Colombia","regionName":"South America","timeZoneRegionName":"America/Bogota","localTime":"2014-03-31T18:51:58.372","utcOffsetHours":-5.0,"latitude":3.543056,"longitude":-76.381389,"elevationFeet":3162,"classification":3,"active":true,"delayIndexUrl":"https://api.flightstats.com/flex/delayindex/rest/v1/json/airports/CLO?codeType=fs","weatherUrl":"https://api.flightstats.com/flex/weather/rest/v1/json/all/CLO?codeType=fs"}
This is the search function:
- (void)filterContentForSearchText:(NSString*)searchText scope:(NSString*)scope
{
// Update the filtered array based on the search text and scope.
// Remove all objects from the filtered search array
[self.filteredCandyArray removeAllObjects];
// Filter the array using NSPredicate
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"SELF.name contains[c] %#",searchText];
NSArray *tempArray = [airportsArray filteredArrayUsingPredicate:predicate];
NSLog(#" text %#", searchText);
filteredCandyArray = [NSMutableArray arrayWithArray:tempArray];
NSLog(#"NSLog %#", scope);
}
What's up with that candy object?
You have an array of dictionnary, here's how you parse that:
Get the array:
NSArray *airportsArray = [json objectForKey:#"airports"];
Set the cell text:
[[cell textLabel] setText:[[airportsArray objectAtIndex:indexPath.row]objectForKey:#"name"]];
[[cell detailTextLabel] setText:[[airportsArray objectAtIndex:indexPath.row]objectForKey:#"code"]];
or for better readability:
NSDictionary *airportAtIndex = [airportsArray objectAtIndex:indexPath.row];
[[cell textLabel] setText:[airportAtIndex objectForKey:#"name"]];
[[cell detailTextLabel] setText:[airportAtIndex objectForKey:#"code"]];
Can you elaborate on how I could use sendAsynch to speed up the process?
Ok, first thing to note, you are not speeding up anything here, the reason why you feel the UI is lagging is because you run the network request on the main thread.
You can solve that problem by sending the request asynchrously, meaning in a background thread which will not freeze your User Interface.
dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void){
//this is where you perform the network request
//You can fetch data, in your case get the JSON
dispatch_async(dispatch_get_main_queue(), ^(void){
//in this block you want to manipulate the the User Interface
//this is where you reload the tableView
//or stop an activity indicator
[self.tableView reloadData];
});
});
Things to note (from #HotLicks)
The app must be set up so that the TableView delegate will initially
(before the data is downloaded) report zero rows in the section. Then,
the reloadData op will cause the TableView to refresh the table. So
initially the table will be blank. One could fudge it slightly to
initially present a single cell saying "Data is loading" or anything that lets the user know >that an operation is in progress such as a UIActivityIndicator.
read up on Grand Central Dispatch (GCD)
You could take several approaches here in order to improve performance.
Start uploading and requesting airports as soon as possible in your application.
Complimentarily try to perform any heavy operation in a background thread, dispatching an asynchronous operation to build you array of Candy objects. You can use dispatch_async.
Another approach would be to have some kind of custom logic to avoid creating the whole array at a time… I would keep for example the JSON result (NSMutableDictionary *routeRes) and would create Candy objects on demand (every time a cell is required), keeping a trace of the last Candy index read / created in the JSON up to finish parsing all the dictionary (then you can start reading your array of candies)…. That could work if Candy creation logic is not too heavy (I think it's not).
I'm new to ios development and I'm trying to sort a tableview that gets populated by an array. I've looked into other solutions and for some reason the tableview isn't populating correctly. The array sorts in ascending order, but then the tableview doesn't display the objects in the correct order.
Here is what I have for the sorting:
NSSortDescriptor *sortDescriptor;
sortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"_miles"
ascending:YES];
NSArray *sortDescriptors = [NSArray arrayWithObject:sortDescriptor];
NSArray *sortedArray = [_addObjects sortedArrayUsingDescriptors:sortDescriptors];
_objects = [NSMutableArray arrayWithArray:sortedArray];
Here is my cellforrow :
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
//NSLog(#"%#", _objects);
VenuesTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"Cell" forIndexPath:indexPath];
CustomObject *currentObject = [_objects objectAtIndex:indexPath.row];
cell.venuesTitle.text = [currentObject nameOfVenue];
NSString *preDistance = #"about";
NSString *postDistance = #"miles";
cell.venuesSubTitle.text = [NSString stringWithFormat:#"%# %# %#", preDistance,[_miles objectAtIndex:indexPath.row] ,postDistance];
if (cell != nil) {
NSMutableString *imagePath = [NSMutableString stringWithFormat:#"%#", [_paths objectAtIndex:indexPath.row]];
[cell.venuesImage setContentMode:UIViewContentModeScaleAspectFit];
[cell.venuesImage setImageWithURL:[NSURL URLWithString:imagePath] placeholderImage:[UIImage imageNamed:#"placeholder#2x.png"]];
}
return cell;
}
when I log out the _objects array I get them in the right order, but they're not appearing in the right order in the tableview (the objects have a miles property that I'm trying to sort by).
You are sorting your array, _addObjects by miles and storing the result in _objects, but then, when you're displaying the results, you're not accessing miles from that sorted array you just created, but rather looking it up in some other array.
I don't know when you're populating that separate _miles array, but I'd suggest you retire it entirely and just make sure you get and set the miles property from the CustomObject instances. (Same for _paths ... this should probably be a property of your CustomObject, not a separate array.)
I don't see anything obvious, but a couple things that are a bit strange:
You should not use _objects. Always use self.objects instead. Most programming languages make it completely impossible to access pointers directly like you are doing here, and for good reason. It's dangerous and can lead to confusing bugs like this one. You should pretty much never access a property using _foobar. Always use self.foobar.
Why are you using NSMutableArray instead of NSArray? Are you sure there isn't code somewhere else modifying it?
Where does VenuesTableViewCell *cell get created? I don't see anywhere in your code that one of these objects is created. [tableView dequeueReusableCellWithIdentifier..] never creates a new cell, it always returns an existing one or nil if none exists. Your code isn't throwing an assertion error (I assume?), so obviously it never returns nil. But there's something weird going on if that doesn't return nil the first time you call it?
I'm working in Storyboard but I presume now it's time to face the code...
I have a PHP file on my server which outputs the contents of my MySQL database table as an array in JSON format:
{
"id":"2",
"name":"The King's Arms",
"city":"London",
"day":"Tuesday",
}
I'll need all the data eventually, but for now I just want to output the city fields as a UITableView. How would I go about doing this?
I believe it was in the year 2012/2013 where Apple covered a fantastic video on code practices, one sub topic highlighted was a good smart way of handling JSON objects and creating data objects for them. I'm sorry I forgot the name of the actual video, if someone remembers it please do edit the answer for me.
What apple covered was to have a data object that stores each json object. We will then create an array to store these objects and access the required fields appropriately when populating our tableView. So in your case you would do something like this.
in your project navigator add a file of NSObject type and add the following code:
#import <Foundation/Foundation.h>
#interface PlaceDataObject : NSObject
-(id)initWithJSONData:(NSDictionary*)data;
#property (assign) NSInteger placeId;
#property (strong) NSString *placeName;
#property (strong) NSString *placeCity;
#property (strong) NSString *placeDay;
#end
and in your .m file you would add this code
#import "PlaceDataObject.h"
#implementation PlaceDataObject
#synthesize placeId;
#synthesize placeName;
#synthesize placeCity;
#synthesize placeDay;
-(id)initWithJSONData:(NSDictionary*)data{
self = [super init];
if(self){
//NSLog(#"initWithJSONData method called");
self.placeId = [[data objectForKey:#"id"] integerValue];
self.placeName = [data objectForKey:#"name"];
self.placeCity = [data objectForKey:#"city"];
self.placeDay = [data objectForKey:#"day"];
}
return self;
}
#end
What you have now is a data object class which you can use everywhere in your code where ever required and grab the appropriate details for whichever table youre showing, whether it be a city fields table or a city and name table etc. By doing this you will also avoid having json decoding code everywhere in your project. What happens when the name of your 'keys' changes? rather than scouring through your code correcting all your keys, you simply go to the PlaceDataObject class and change the appriopriate key and your application will continue working.
Apple explains this well:
"Model objects represent special knowledge and expertise. They hold an application’s data and define the logic that manipulates that data. A well-designed MVC application has all its important data encapsulated in model objects.... they represent knowledge and expertise related to a specific problem domain, they tend to be reusable."
Populating your array with custom objects for every json entry that comes in from the server
Now onto populating an array of this custom data object you've made. Now following the MVC approach, it's probably best that you have all your methods that process data in a different class, your Model class. That's what Apple recommends to put these kind of methods in a seperate model class where all the processing happens.
But for now we are just going to add the below code to the View Controller just for demonstration purposes.
Create a method in your view controller's m file that will process your JSON array.
//make sure you have a global NSMutableArray *placesArray in your view controllers.h file.
-(void)setupPlacesFromJSONArray:(NSData*)dataFromServerArray{
NSError *error;
NSMutableArray *placesArray = [[NSMutableArray alloc] init];
NSArray *arrayFromServer = [NSJSONSerialization JSONObjectWithData:dataFromServerArray options:0 error:error];
if(error){
NSLog(#"error parsing the json data from server with error description - %#", [error localizedDescription]);
}
else {
placesArray = [[NSMutableArray alloc] init];
for(NSDictionary *eachPlace in arrayFromServer)
{
PlaceDataObject *place = [PlaceDataObject alloc] initWithJSONData:eachPlace];
[placesArray addObject:place];
}
//Now you have your placesArray filled up with all your data objects
}
}
And you would call the above method like so:
//This is not what your retrievedConnection method name looks like ;)
// but you call the setupPlacesFromJSONArray method inside your connection success method
-(void)connectionWasASuccess:(NSData *)data{
[self setupPlacesFromJSONArray:data];
}
Populating your tableView with your custom Data objects
As for populating your data in your TableView you do so like this:
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
//We check against table to make sure we are displaying the right number of cells
// for the appropriate table. This is so that things will work even if one day you
//decide that you want to have two tables instead of one.
if(tableView == myCitysTable){
return([placesArray count]);
}
return 0;
}
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"Cell"];
if(cell)
{
//set your configuration of your cell
}
//The beauty of this is that you have all your data in one object and grab WHATEVER you like
//This way in the future you can add another field without doing much.
if([placesArray count] == 0){
cell.textLabel.text = #"no places to show";
}
else{
PlacesDataObject *currentPlace = [placesArray objectAtIndex:indexPath.row];
cell.textLabel.text = [currentPlace placeCity];
// in the future you can grab whatever data you need like this
//[currentPlace placeName], or [currentPlace placeDay];
}
return(cell);
}
Short disclaimer: the code above has not been tested, but please let me know if it all works well or if I've left out any characters.
If the raw data from you server arrives in a NSData object, you can use NSJSONSerialization class to parse it for you.
That is:
NSError *error;
NSMutableArray *cityArray = [[NSMutableArray alloc] init];
NSArray *arrayFromServer = [NSJSONSerialization JSONObjectWithData:dataFromServer options:0 error:error];
if(arrayFromServer)
{
NSLog(#"error parsing data - %#", [error localizedDescription]);
} else {
for(NSDictionary *eachEntry in arrayFromServer)
{
NSString *city = [eachEntry objectForKey:#"city"];
[cityArray addObject: city];
}
}
Once you're done populating your array of cities, this is what you can return in the table view's data source methods:
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return 1; // your table only has one section, right?
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
if(section == 0)
{
return([cityArray count]);
}
NSLog(#"if we're at this place in the code, your numberOfSectionsInTableView is returning something other than 1");
return 0;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
// are you using standard (default) cells or custom cells in your storyboard?
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"Cell"];
if(cell)
{
cell.text = [cityArray objectAtIndex:indexPath.row];
}
return(cell);
}
what is myCitysTable, Paven? is that suppose to be the name of the TableView? i'm struggling to figure out how to name it, if so...
I have a plist (an array of dictionary's) that I am reading into an NSArray which I am using to populate a table. It's a list of people and their work location, phone number, etc. I added a UISearchBar and implemented the textDidChange method as well.
When I search by the person's last name I do see the filtered list in the table, however I don't think that I am storing the filtered results properly. I am using an NSMutable Array but I am losing the key:value pairing.
Can someone please point me in the right direction? I ultimately would like to click on a filtered name and push to a detailed view controller. I believe my issue is that I am trying to capture the filtered results in an NSMutableArray but I am not certain.
I've done a lot of Googling but can't seem to put this together in my head. Any help is appreciated.
Kind Regards,
Darin
Here is the array that I am using to load the plist.
-(NSArray *)content
{
if (!content){ {
content = [[NSArray alloc] initWithContentsOfFile:[[NSBundle mainBundle] pathForResource:#"ophonebook" ofType:#"plist"]];
NSSortDescriptor* nameSortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"Last" ascending:YES];
content= [content sortedArrayUsingDescriptors:[NSArray arrayWithObject:nameSortDescriptor]];
}
return content;
}
Here is the UISearchBar Method
- (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText {
if (searchText.length == 0)
{
isFiltered= NO;
} else {
isFiltered= YES;
filteredPeople = [[NSMutableArray alloc]init];
for (NSDictionary *dict in content)
{
NSString *last = [dict objectForKey:#"Last"];
NSRange lastRange = [last rangeOfString:searchText options:NSCaseInsensitiveSearch];
if (lastRange.location != NSNotFound)
{
[filteredPeople addObject: [dict objectForKey:#"Last"]];
}
}
}
[myTableView reloadData];
}
Keep things simple, get rid of the flags and the different arrays in your table delegate methods. Instead, have 1 array for your source data (content) and another array for your source data that is actually for display (call it displayContent).
Now, when you start up, set displayContent = content.
Then, when you want to filter, set displayContent = [content filteredArrayUsingPredicate:...]; (you can convert your current loop into a simple predicate).
Finally, when you're done searching, set displayContent = content.
No flags. No ifs in the table delegate methods. Simple, readable code.
p.s. your problem is:
[filteredPeople addObject: [dict objectForKey:#"Last"]];
which you should be setting to:
[filteredPeople addObject:dict];
so you have all the data instead of just the names. Though technically you could still make it work by searching for the last name in your content.