Populate UITableView with JSON array - ios

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...

Related

searchBar method doesn't work with my NSMutableArray (Obj-C)

My app retrieves data in JSON format and displays it in a table view. I'm trying to implement a search method but the app crashes. Any ideas?
I can't seem to find any solutions that work.
Update:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"Cell" forIndexPath:indexPath];
if (!isFiltered) {
Product *productObject;
productObject = [productsArray objectAtIndex:indexPath.row];
cell.textLabel.text = productObject.prodName;
//Accessory.
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
} else {
Product *productObject;
productObject = [self.filteredProductNameArray objectAtIndex:indexPath.row];
//Accessory.
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
}
return cell;
}
You are filling the productsArray with objects type of Product
[productsArray addObject:[[Product alloc] initWithProdID:pID andProdName:pName andProdDescription:pDescription andProdImage:pImage andProdManufacturer:pManufacturer andProdQuantity:pQuantity andProdPrice:pPrice]];
And then searching for NSString in it: for (NSString *str in productsArray).
Correct way of searching would be when you create separate array containing list of items to be search. E.g Array of products name, or description.
Simple example to approach
#property (strong, nonatomic) NSMutableArray *filteredProductNameArray;
So here this property will store Array of product's name
- (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText {
NSMutableArray *productsNameArray = [[NSMutableArray alloc] init];
if (searchText.length == 0) {
isFiltered = NO;
self.filteredProductNameArray = self.productsArray; //If no str in textfield you should display all the data
}
else {
isFiltered = YES;
for (Product *product in self.productsArray) { //Because you have array type of Product, not NSString
NSString *productName = product.name; //Or method to access `name` property of the Product class
NSRange stringRange = [productName rangeOfString:searchText options:NSCaseInsensitiveSearch];
if (stringRange.location != NSNotFound) {
[productsNameArray addObject:productName];
}
}
}
self.filteredProductNameArray = productsNameArray;
// Here you got array of products name which matches string in search bar textfield. And it is the actual products name list to be displayed
[self.tableView reloadData];
}
Here. And in your
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
}
You should check whether the name property of self.productsArray at indexPath.row equals to self.filteredProductNameArray;
I've left the further steps, but I guess you've got the idea and can complete by yourself. If you have any questions feel free to ask, I will try to help
Your app crashes because productsArray contains objects that are of type Product not NSString.
So in your for loop change this:
for (NSString *str in productsArray)
into this
for (Product *product in productsArray)
Then get the NSString property of the product. I think you mean productName
you are using two different NSMutableArray for retrieveData and searchBar, try to use same which you have used in Table row and creating cell.
It may have something to do with the method you are using to fetch the JSON. dataWithContentsOfURL: should not be used for network-based URLs. dataWithContentsOfURL:
To fetch data over the network, take a look at NSURLSession.
Where is it crashing exactly? I suggest putting in a break point and stepping through this code. That will probably be the easiest way to find the source.

how to populate table view cell or controller with rest api call using objective c

https://github.com/svenanders/iOS7-Rest-Example-App
i tried out this coding...but it doesn't works for my url ...my data format like this...
{"wk_times":[{"user":{"id":1,"name":"Redmine Admin"},"hours":11.0,"startdate":"2015-07-27","status":"n"},{"user":{"id":1,"name":"Redmine Admin"},"hours":42.0,"startdate":"2015-07-20","status":"n"}
i have to show this data in a table view cell like:
startdate | status | username
could anyone help me....regarding this...i am new to ios development
For now just to resolve your problem follow this. Better you can look at some UITableView tutorials and JSON parsing, and learn about NSDictionary and NSArray. This will perfectly help you to do things.
As per the source code. In completion handler block defined in fetchRestData after the JSON Serialisation. like follows:
NSDictionary *rest_data = [NSJSONSerialization JSONObjectWithData:data
Create an NSArray on your ViewController interface as:
#property (nonatomic, strong) NSArray *users;
As per your response JSON you have mentioned in the question, You need to get the users in an array like:
self.users = [NSArray arrayWithArray:[rest_data objectForKey:#"wk_times"]];
Now you have list of users to be displayed in your table view.
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.users.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
// Create cell to display the data.
NSDictionary *userDict = self.users[indexPath.row]; // This userDict will have this {"user":{"id":1,"name":"Redmine Admin"},"hours":11.0,"startdate":"2015-07-27","status":"n"}
NSDictionary *user = userDict[#"user"]; // This user will have {"id":1,"name":"Redmine Admin"}
NSString *name = user[#"name"];
NSString *userId = user[#"id"];
// Using these data you set to the table cells.
}
This may help you.

How to Parse data from CSV File & populate UITableview in Objective-C?

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.

Creating an Array for cellForRowAtIndexPath with Other Methods

I have an indexed tableView that displays a list of songs from the user's iPod library. This is how I currently get the song titles, which causes scrolling to be very slow:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
cell.textLabel.text= [self titleForRow:indexPath]; //getting cell content
}
...which calls these methods:
-(NSString *)titleForRow:(NSIndexPath *)indexpath{
NSMutableArray* rowArray=[[NSMutableArray alloc]initWithCapacity:0];
rowArray=[self getArrayOfRowsForSection:indexpath.section];
NSString *titleToBeDisplayed=[rowArray objectAtIndex:indexpath.row];
return titleToBeDisplayed;
}
-(NSMutableArray *)getArrayOfRowsForSection:(NSInteger)section
{
NSString *rowTitle;
NSString *sectionTitle;
NSMutableArray *rowContainer=[[NSMutableArray alloc]initWithCapacity:0];
for (int i=0; i<self.alphabetArray.count; i++)
{
if (section==i) // check for right section
{
sectionTitle= [self.alphabetArray objectAtIndex:i]; //getting section title
for (MPMediaItem *song in songs)
{
NSString *title = [song valueForProperty:MPMediaItemPropertyTitle];
rowTitle= [title substringToIndex:1]; //modifying the statement to its first alphabet
if ([rowTitle isEqualToString:sectionTitle]) //checking if modified statement is same as section title
{
[rowContainer addObject:title]; //adding the row contents of a particular section in array
}
}
}
}
return rowContainer;
}
The problem is: for each cellForRowAtIndexPath, I am calling these methods and creating these arrays for each cell. So I need to create a separate array and simply call the objectAtIndex from that array.
This is what I have tried so far: created an NSMutableArray *newArray, and the following method:
-(NSMutableArray *)getNewArray:(NSInteger)section
{
NSString *rowTitle;
NSString *sectionTitle;
for (int i=0; i<self.alphabetArray.count; i++)
{
if (section==i) // check for right section
{
sectionTitle= [self.alphabetArray objectAtIndex:i]; //getting section title
for (MPMediaItem *song in songs)
{
NSString *title = [song valueForProperty:MPMediaItemPropertyTitle];
rowTitle= [title substringToIndex:1]; //modifying the statement to its first alphabet
if ([rowTitle isEqualToString:sectionTitle]) //checking if modified statement is same as section title
{
[newArray addObject:title]; //adding the row contents of a particular section in array
}
}
}
}
return newArray;
}
But this seems very wrong and I'm really confused on how to solve this. I have no clue how to create a separate array with these methods, populating newArray with the song titles, and I've searched Google for quite some time now and I can't find anything that would help me.
Could somebody point me in the right direction, or please show me how I'd creat newArray? I've attached my tableView data source here. Thanks. Any help would be much appreciated.
Table View cells are enqueued and dequeued in a serial manner. Leveraging this ability you should expect that only sections change in less frequency. You can use static variables to exempt the fetching of a section. Look at my code
-(NSString *)titleForRow:(NSIndexPath *)indexpath{
static NSIndexPath *funIndexPath;
static NSMutableArray *funSectionArray;
if (!funIndexPath || funIdexpath.section != indexpath.section) {
funIndexPath = indexpath;
funSectionArray = [self getArrayOfRowsForSection:indexpath.section];
}
rowArray = funSectionArray;
NSString *titleToBeDisplayed = [rowArray objectAtIndex:indexpath.row];
return titleToBeDisplayed;
}
This is the first optimisation possible.
When I look at your fetching results array, some things are not clear. Why are you iterating to check section == i. You can just substitute section for i in your code.
-(NSMutableArray *)getNewArray:(NSInteger)section {
NSString *rowTitle;
NSString *sectionTitle;
sectionTitle = [self.alphabetArray objectAtIndex:section]; //getting section title
for (MPMediaItem *song in songs) {
NSString *title = [song valueForProperty:MPMediaItemPropertyTitle];
rowTitle= [title substringToIndex:1]; //modifying the statement to its first alphabet
if ([rowTitle isEqualToString:sectionTitle]) //checking if modified statement is same as section title
{
[newArray addObject:title]; //adding the row contents of a particular section in array
}
}
return newArray;
}
Its still not clear what you are trying to achieve here.
Going against my own answer, I suggest you that you do not compute you data for display during view rendering time. You need to create the cells as fast as possible. Why not create a property or an iVar (like #bauerMusic is Suggesting) and compute the date in viewDidLoad or declare a custom function which will reload your property (possible an Array or Dictionary) and while rendering the UI, just set the values (in you viewForIndexPath)?
You can use an array of two level depth to store for maintaining sections and rows. i.e. Level 1 - Sections and Array of rows, Level 2 - row.
The center of it all is:
cell.textLabel.text = [self titleForRow:indexPath];
Now, what you're actually using are NSString. Simple. Instead, you're creating a new array for each cell each call??
Why not have one array, say an NSMutableArray. Have it as a #property or local iVar.
#property (strong, nonatomic) NSMutableArray *rowArray;
// Instantiate in -viewDidLoad
rowArray = [NSMutableArray array]; // Much more elegant IMHO
Now call a method that do all the content loading, but do it ONCE. You can always call it again to refresh.
Something like:
// In -viewDidLoad
rowArray = [NSMutableArray array];
[self loadArrayContent];
Then, your cellForRowAtIndexPath should only be using your preloaded rowArray and simply pulling NSString titles from it.
My general way to handle a table view is to create (during table view setup) an NSMutableArray for the rows (anchored in the data source object). Each entry in the array is an NSMutableDictionary that is "primed" with the basic stuff for the row, and if more info needs to be developed to display the row, that is cached in the dictionary. (Where there are multiple sections I use an array of sections containing arrays of rows.)
What I might do for a play list is initialize the array with empty dictionaries, then, as one scrolls, access each play list element and cache it in the dictionary.
In fact, if one really wanted, updating the dictionary could be done via a background task (with appropriate locking) so that scroll speed wouldn't be hampered by the lookup operations.

UITableView appearing before getting data from connectionDidFinishLoading

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];
});

Resources