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.
Related
I have a table view that pulls data from my database, and displays it in my tableview cells. The values returned are:
This is the messages data (
{
body = "13-10-2017 12:12";
name = "Leeya";
},
{
body = "09-10-2017 19:37";
name = Leeya;
},
{
body = "test";
name = "Jessica";
}
That said, when my dictionary comes back with multiple arrays containing the same 'name' value (as it is in this case, 'Leeya'), I only want my tableview to display the first of these arrays (one cell), much like in a chat app when you see a list of conversations.
The code I have currently accomplishes this, but it literally only returns ONE cell and ONE array, populated by the most recently posted data. E.g. tableview returns cell:
LEEYA
When it should return:
LEEYA
JESSICA
See my code below. I assume this is because the line
NSString *nodeTitle = self.messages[0][#"name"];
is always equal to one username, and thus it returns one value.
So my question is: How can I make that line function so that, "If 'name' value in dictionary self.messages appears more than once, only show the first value" ? Or in other words: If 'name' value is equal to 'name' in arrays that follow, only show the first?
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
NSString *nodeTitle = self.messages[0][#"name"];
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"name == %#", nodeTitle];
NSArray *filteredArray = [self.messages filteredArrayUsingPredicate:predicate];
id firstFoundObject = nil;
firstFoundObject = filteredArray.count > 0 ? filteredArray.firstObject : nil;
NSDictionary *firstObjects = firstFoundObject;
NSLog(#"The first objects are %#", firstObjects);
static NSString *PointsTableIdentifier = #"MyMessagesCell";
MyMessagesCell *cell = (MyMessagesCell *)[tableView dequeueReusableCellWithIdentifier:PointsTableIdentifier];
if (cell == nil)
{
NSArray *nib = [[NSBundle mainBundle] loadNibNamed:#"MyMessagesCell" owner:self options:nil];
cell = [nib objectAtIndex:0];
}
I hope I explained this clearly. Thanks!
I would suggest not trying to do this in cellForRowAtIndexPath.
Instead, when you first get your data from the server, write a method that takes the input array and filters it to remove duplicates. Something like this:
- (NSArray *) UniqueEntriesForArray: (NSArray *) sourceArray {
NSMutableArray *results = [NSMutableArray array];
NSMutableSet *uniqueStrings = [NSMutableSet new];
for (NSDictionary *element in sourceArray) {
NSString *name = element[#"name"];
if (![uniqueStrings contains: name]) {
[uniqueStrings addObject: name];
[results addObject: element];
}
}
return [results copy];
}
(My Objective-C is getting a little rusty so that might need some adjustment. It would also be cleaner if you used the new typed array syntax for Objective-C, but I don't remember that off the top of my head.)
My tableview is populated with cells that show an item with its name, price, and brand. Some objects that I get from the web service return , and that looks ugly on the table view cell. I don't want to populate table view cells that have a price of "null". Here is my code so far. For now, I change it to say "Price Unavailable".
for (NSDictionary *objItem in resultsArray)
{
NSString *currentItemName = [objectTitlesArray objectAtIndex:indexPath.row];
if ([currentItemName isEqualToString:objItem[#"title"]])
{
if (cell.priceLabel.text != (id)[NSNull null] && cell.priceLabel.text.length != 0 && cell.brandLabel.text != (id)[NSNull null] && cell.brandLabel.text.length != 0)
{
cell.nameLabel.text = currentItemName;
NSDictionary *bestPageDictionary = objItem[#"best_page"];
NSNumber *price = bestPageDictionary[#"price"];
if ((cell.priceLabel.text = #"<null>"))
{
cell.priceLabel.text = #"Price Unavailable";
}
else
{
cell.priceLabel.text = [NSString stringWithFormat:#"$%#", price];
}
NSArray *brandsArray = objItem[#"brands"];
cell.brandLabel.text = [brandsArray firstObject];
}
}
}
This is terribly inefficient. You are keeping the JSON (I'm assuming) return in a dictionary, then looping over the dictionary for EVERY cell that you're creating. Not only that, you aren't cleaning up the JSON ahead of time.
It's much more expensive to create a useless cell then go back and try to delete it. In your numberOfRowsInSection delegate, you've already told the tableview that you have X many cells. Now you're trying to delete the cells which will mess up the callbacks. You'd have to create a method that will run after you finish creating all cells that will then loop through all your cells to delete them from the tableView, which will then call [table reloadData]. However, because you actually aren't removing the data from the NSDictionary, you actually will create the same amount of cells again and be stuck in an infinite loop.
Solution:
First, change your structure. Once you get the JSON return, sanitize it to delete all values with no price. I suggest also using an object class to hold each server object. This will simplify your code a lot for the TableView as well as other classes as well. Now that you've cleaned up your return, change it from a NSDictionary into a NSMutableArray. Then in numberOfRowsInSection: you call [array count]. In cellForRowAtIndexPath: you simply look at [array objectAtIndex:indexPath.row] to get your object.
You'll want code to generally look like:
ServerItem.h : NSObject{
#property (nonatomic,retain) NSString* name;
...
add in other properties here
}
- (NSMutableArray *) parseJSON:(NSDictionary *)jsonDict{
NSMutableArray *returnArray = [NSMutableArray array];
NSArray *dictArray = [NSArray arrayWithArray:jsonDict[#"results"]];
for (NSDictionary *itemDict in dictArray)
{
NSDictionary *bestPageDictionary = objItem[#"best_page"];
if (![bestPageDictionary[#"price"] isEqualToString:#"<null>"]])
{
ServerItem item = [ServerItem new];
item.price = ...
item.name = ....
[returnArray addObject:item];
}
}
return returnArray;
}
Inside your webService code:
self.itemArray = [self parseJSON:dataDictionary];
Then
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return [self.itemCallArray count]
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
...
ServerItem *cellItem = [self.itemsArray objectAtIndex:indexPath.row];
cell.nameLabel.text = cellItem.name;
cell.priceLabel.text = cellItem.price;
...
}
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.
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'm trying to create a search bar with a name auto complete that looks something like this in the image.
Does anyone have an idea how I may be able to achieve this?
Have a look at the TTMessageController and the TTMessageRecipientField from the open source library Three20. It's just what you want:
First you need an array with the contact's names:
//Add Address Book framework
#import <AddressBook/AddressBook.h>
--------------------
ABAddressBookRef addressBook = ABAddressBookCreate();
NSArray *allPeople = (__bridge_transfer NSArray *)ABAddressBookCopyArrayOfAllPeople(addressBook);
NSMutableArray *firstNames = [NSMutableArray alloc] init];
for(NSUInteger personIndex = 0; personIndex <= [allPeople count]; personIndex++){
ABRecordRef person = (__bridge ABRecordRef)[allPeople objectAtIndex: incrementer];
firstNameString = (__bridge_transfer NSString *)ABRecordCopyValue(person, kABPersonFirstNameProperty);
[firstNames addObject: firstNameString];
}
So the firstNames array now contains an array of all the people;
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
UITableViewCell *cell = [self.tableView dequeueReusableCellWithIdentifier:#"MyIdentifier"];
if (cell == nil)
{
cell = [[UITableViewCell alloc] initWithFrame:CGRectMake(your, values, go, here) reuseIdentifier:#"MyIdentifier"];
}
cell.text = [firstNames objectAtIndex: indexPath.row];
}
So far, you have a tableview with all the first names. Make sure you implement the other required methods in the protocols UITableViewDataSource and UITableViewDelegate. Also make sure that you declare the firstNames array a property in your header file to make sure it is accessible throughout the .m file, because if you do not do that, then you cannot access it in the cellForRowAtIndexPath: method.
To implement autocompletion, follow Ray Wenderlich's tutorial here on custom autocompletion values. Then use the array of peoples' first names as the custom values.