This question already has answers here:
Closed 11 years ago.
Possible Duplicate:
addObject: to array not working (array still nil)
EVERYTHING UPDATED
This app is a table view with a tab bar controller. I am logging the count of the array: arrayOfFavourites and even though i add an object is continues to have a nil value, my relating code, all objects shown are allocated and initialized in the code (previous or present) some are instances and some are properties:
ListViewController.m:
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
NSLog(#"TOUCHED CELL!");
// Push the web view controller onto the navigation stack - this implicitly
// creates the web view controller's view the first time through
[[self navigationController] pushViewController:webViewController animated:YES];
// Grab the selected item
entry = [[channel items] objectAtIndex:[indexPath row]];
if (!entry) {
NSLog(#"!entry");
}
// Construct a URL with the link string of the item
NSURL *url = [NSURL URLWithString:[entry link]];
// Construct a request object with that URL
NSURLRequest *req = [NSURLRequest requestWithURL:url];
// Load the request into the web view
[[webViewController webView] loadRequest:req];
// Take the cell we pressed
// IMPORTANT PART
CELL = [tableView cellForRowAtIndexPath:indexPath];
[webViewController setItem:entry];
webViewController = nil;
webViewController = [[WebViewController alloc] init];
[entry release];
}
WebViewController.m:
You shake to favorite a cell
-(void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event
{
cellToPassOn = nil;
NSLog(#"Favouriting"); // YES I KNOW SPELLING
// This is pretty simple, what we do is we take the cell we touched and take its title and link
// then put it inside an array in the Favourites class
Favourites *fav = [[Favourites alloc] init];
ListViewController *list = [[ListViewController alloc] init];
[self setCellToPassOn: [list CELL]];
if (!item) {
NSLog(#"NILLED ITEM");
}
[[fav arrayOfFavourites] addObject:[item autorelease]];
[fav setCell: cellToPassOn];
}
Favourites.m:
-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
NSLog(#"ROWS NO.");
NSLog(#"%i", [arrayOfFavourites count]);
return [arrayOfFavourites count];
}
Favourites:
A CLASS, ON TAB BAR CONTROLLER
WEBVIEWCONROLLER:
CONTROLLER FOR DIFFERENT WEB VIEWS
LISTVIEWCONTROLLER:
DATA PROVIDER
Actually what happens is when i shake i reload the table view data and i add an object to an array (array of favorites) the count is one.... GOOD! But then when i shake again (in a different article, my app has different webViews when i press different cells.) It is still 1... weird.... if i go to favorites class that array remains one.. ok... so as you can see i am returning the arrayOfFavourites count to numberOfRowsInSection (which is 1) but no cells appear and the cellForRowAtIndexPath is never called (using an NSLOG) why is this happening I AM VERY ANNOYED!
In your Favourites.m numberOfRowsInSection function, looks like you should do this:
if(arrayOfFavourites == NULL)
{
arrayOfFavourites = [[NSMutableArray alloc] init];
}
Because you're reinitializing (and likely leaking) in every single call to numberOfRowsInSection (which gets called each time the table needs to know how many rows it must display -- i.e. very often).
You create and destroy a new Favourites object every time you go through -motionBegan:withEvent:, and you create a new array every time you go through -tableView:numberOfRowsInSection:. If you want data to persist beyond those events, you need to keep the objects around.
Let review this part of your code :
-(void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event {
cellToPassOn = nil;
NSLog(#"Favouriting"); // YES I KNOW SPELLING
// This is pretty simple, what we do is we take the cell we touched and take its title and link
// then put it inside an array in the Favourites class
// HERE you are creating a NEW fav
Favourites *fav = [[Favourites alloc] init];
// HERE you are creating a NEW list
ListViewController *list = [[ListViewController alloc] init];
// SO HERE what is "CELL" doing, returning some constant or static object?
[self setCellToPassOn: [list CELL]];
// HERE what is item and where does it come from
if (!item) {
NSLog(#"NILLED ITEM");
}
// Here you take an array of an object you just created and autoreleasing the item
// this is not the regular way to handle memory management in Cocoa,
// depending on what you are doing to item else where you could get item == deallocated pretty soon
[[fav arrayOfFavourites] addObject:[item autorelease]];
[fav setCell: cellToPassOn];
HERE you are releasing fav
[fav release];
HERE fav don't exist anymore as well as the array to which you've added something to it.
[list release];
item = nil;
}
Unless I'm reading your code incorrectly I have the feeling that you are trying to have some persistence with "volatile" object.
You need to make a #property for those object to survive longer that one method call.
Every time you create a new object it's new and have no knowledge of the precedent one.
And if we look at this code :
-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
// HERE everytime the tableview is asking for the numberOfRowsInSection, you create a new array
// And that new empty array is replacing your old one.
// That is your "weird" thing, it's doing what you are asking it to do, set it back to a new empty array.
arrayOfFavourites = [[NSMutableArray alloc] init];
NSLog(#"ROWS NO.");
NSLog(#"%i", [arrayOfFavourites count]);
return [arrayOfFavourites count];
}
Related
I have a to-do list app with a bunch of tasks. Each task has a UITableViewCell. After each table view cell is tapped, it creates a view controller with the task at that row's index path's property. These view controllers are all stored in a NSDictionary. This is the code representation of what I just said:
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{
DetailViewController *detailVC;
if (![self.detailViewsDictionary.allKeys containsObject:indexPath]){
detailVC = [[DetailViewController alloc]initWithNibName:#"DetailViewController" bundle:nil];
[self.detailViewsDictionary setObject:detailVC forKey:indexPath];
detailVC.context = self.managedObjectContext;
}else{
detailVC = self.detailViewsDictionary[indexPath];
}
Tasks *task = [[self fetchedResultsController] objectAtIndexPath:indexPath];
detailVC.testTask = task;
[[self navigationController] pushViewController:detailVC animated:YES];
NSLog(#"%#", self.detailViewsDictionary);
}
So this method of creating unique view controllers and storing them with a certain key almost always works. The problem arises when I delete or move the view controllers:
I was under the impression that a cell's gets recycled as you scroll down (dequeue). This means that marking each cell with a number identifier would result in multiple cells for the same identifier.
Also, if you stored each view controller with a indexPath key, how do you make sure the key isn't set to two view controllers..? For example. Let's say you have 4 cells, which means 4 view controllers. You delete cell 3. Cell 4 moves down to cell 3s spot. You create a new cell which goes to spot 4. Now you have two controllers with the same indexPath key! How do you avoid this?? It's screwing up my app right now because tasks that have already been moved are loading their properties in the wrong view controller/cell!
I was suggested this to solve the problem before: "You maintain an NSMutableArray that "shadows" the contents of the table." I was also suggested to use tags. However, I don't understand what how to implement these.
edit: ---random string---
In tasks.m
-(NSString *)uniqueIdentifierString{
static NSString *alphabet = #"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXZY0123456789";
NSMutableString *s = [NSMutableString stringWithCapacity:20];
for (NSUInteger i = 0U; i < 20; i++) {
u_int32_t r = arc4random() % [alphabet length];
unichar c = [alphabet characterAtIndex:r];
[s appendFormat:#"%C", c];
}
return s;
}
In tableviewcontroller.m
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{
DetailViewController *detailVC;
Tasks *task = [[self fetchedResultsController] objectAtIndexPath:indexPath];
if (![self.detailViewsDictionary.allKeys containsObject:task.uniqueIdentifierString]){
detailVC = [[DetailViewController alloc]initWithNibName:#"DetailViewController" bundle:nil];
[self.detailViewsDictionary setObject:detailVC forKey:task.uniqueIdentifierString];
detailVC.context = self.managedObjectContext;
}else{
detailVC = self.detailViewsDictionary[task.uniqueIdentifierString];
}
detailVC.testTask = task;
[[self navigationController] pushViewController:detailVC animated:YES];
NSLog(#"%#", detailVC);
NSLog(#"%#", task.uniqueIdentifierString);
}
One solution would be to decouple your VCs from the table cells. You could store the VCs in your dictionary referenced by an ID that is unique to the task, but not worry about the table cell. If the task is moved or deleted, you can handle that elsewhere without worrying about the VC array being instantly wrong.
Your other option would be to hook into the move/edit methods the logic for iterating over the VC array to adjust the array at that time.
Given that UITableView expects cells to be reused, you can see how it isn't meant to be a storage mechanism for other data.
It would be best to decouple the data so that you aren't bound to the order or existence of them.
I have few table view controllers and I want selections from them to be shown on "ResultTableViewController". I also have array that collects the selected data and when i finally push it to the "ResultViewController" it shows only the last selection. Please help
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
firstarr = [[NSMutableArray alloc]init]; //array with questions
[self.firstarr addObject:[self.data objectAtIndex:indexPath.row]]; //collects the data properly
BodyDetailViewController* vc =[[BodyDetailViewController alloc] initWithNibName:#"BodyDetailViewController" bundle:nil];
vc.someArray = firstarr; //some array - ViewController with Results.
[self.navigationController pushViewController:vc animated:YES];
}
You have to remove this line :
firstarr = [[NSMutableArray alloc]init];
Because you're initializing your NSMutableArray each time you're selecting a cell, so all the data previously stored is erased and vc.someArray is getting an array with only your last selection, that's why ;)
Init your NSMutableArray outside the method, in your viewDidLoad for example
This...
firstarr = [[NSMutableArray alloc]init]; //array with questions
...creates a new array. Whatever was in firstarr previously is gone when you do a new selection.
Either create the array once somewhere else (when the object is initialized?) or append its content to vc.someArray instead of replacing it.
I've read some of the other posts but haven't found the right solution that seems to work in my case. I've got a UITableViewController class that is instantiated from the Main App Delegate and it controls other views that get pushed into the UINavigationController instance created from the App Delegate.
On the UINavigationToolBar I have a UISegmentedControl button that is split in two pieces, Factory Loads and User Loads. I have an action method attached to the SegmentedControl to detect when the button is pressed and that method is inside the UIViewController Class. My goal is to have the view reload with the contents of a new dictionary that is constructed from a plist file in the app bundle. I have the dictionary loaded, and the keys to be displayed set up in the respective data formats, but when I try to reload the UITableView, it crashes.
This is the last code I tried to get to work inside my action method, but it still doesn't function as intended.
- (void) action:(id)sender {
UISegmentedControl *segment = (UISegmentedControl *) sender;
if ([segment selectedSegmentIndex] == 1)
{
NSLog(#"User Load Selected");
NSString *userLoadFilePath = [[NSBundle mainBundle] pathForResource:#"UserLoads" ofType:#"plist"];
NSDictionary *userLoadDictionary = [[NSDictionary alloc] initWithContentsOfFile:userLoadFilePath];
NSArray *allKeys = [userLoadDictionary allKeys];
keys = allKeys;
dictionary = userLoadDictionary;
[[self tableView] reloadData];
}
else if ([segment selectedSegmentIndex] == 0)
{
NSLog(#"Factory Load Selected");
NSString *factoryLoadFilePath = [[NSBundle mainBundle] pathForResource:#"AlliantPowder" ofType:#"plist"];
NSDictionary *factoryLoadDictionary = [[NSDictionary alloc] initWithContentsOfFile:factoryLoadFilePath];
NSArray *allKeys = [factoryLoadDictionary allKeys];
keys = allKeys;
[[self tableView] reloadData];
}
}
I'm calling [[self tableView] reloadData] in an attempt to retrieve the actual table contained within the UIViewController to try to get the table to reload, but no luck. Any help is appreciated, and if more code is needed please ask. Thanks!
Andrew
------- Edit -----------
Here is the new code in reference to Chip's ideas. The issue is still not resolved and the app still crashes.
- (void) action:(id)sender {
// Create an instance of UISegmentedControl and set it as the sending event.
UISegmentedControl *segment = (UISegmentedControl *) sender;
// Detect which button was pressed and load a new dictionary appropriately
// Checking if userLoads was selected
if ([segment selectedSegmentIndex] == 1)
{
NSLog(#"User Load Selected");
keys = userKeys;
[[self tableView] reloadData];
}
// Checking if factoryLoads was selected
else if ([segment selectedSegmentIndex] == 0)
{
NSLog(#"Factory Load Selected");
keys = [dictionary allKeys];
[[self tableView] reloadData];
}
[segment release];
}
I would pre-load the dictionaries and hold them as ivars in the controller class.
I am betting that the loading of the dictionary contents is not complete, so you are in effect calling reloadData while the data is changing which is causing your crash.
Also, the read is an expensive option and you are loading each time the segementControl changes state.
I’m populating a UITableView based on the values of an NSMutableArray. This table view has search results. If the user clicks in one of the results, one will navigate to another screen. If the user clicks “back”, the search results are filled in again. At this point, while the table view is being repopulated, the old values still appear, just as I want. However, since I’m doing:
- (void)viewWillAppear:(BOOL)animated
{
NSMutableArray *m = [[NSMutableArray alloc] init];
self.searchResultsArray = m;
[m release];
}
The old cells information is no longer available. Thus, the app crashes if the user clicks in one of the old cells or scrolls the UITableView because I’m accessing the mutable array which was reinitialized above.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
NSArray *cellArray = [searchResultsArray objectAtIndex:indexPath.row];
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
appDelegate.selectedCell = [searchResultsArray objectAtIndex:indexPath.row];
}
Do you have any suggestions concerning how should I do this properly?
Thanks.
this is because you are re-initalizing the array each time the view comes back in focus, the array will then have 0 objects in, causing the issue when you select a row and reference an index in the array that simply was wiped when the viewWillAppear is called.
why not init ' self.searchResultsArray ' in viewDidLoad (remembering to undo this with release when the device receives memory warning)
let me know how you get on.
You should reset the array only when the view loads:
- (void)viewDidLoad
{
[super viewDidLoad];
NSMutableArray *m = [[[NSMutableArray alloc] init] mutableCopy];
self.searchResultsArray = m;
[m release];
}
You should always call the reloadData: method when changing the data in the UITableViewDataSourceDelegate instance.
Let me know if this helps
Please try below code:
- (void)viewWillAppear:(BOOL)animated
{
// Your search result array with your old values
// Now add or appened new data to your old search results.
[self.searchResultsArray addObject:#"Your Value 1"];
[self.searchResultsArray addObject:#"Your Value 2"];
[self.searchResultsArray addObject:#"Your Value 3"];
[self.tableView reloadData];
// if you need to add new values from a array put this code in a loop.
}
It should helps you...
Thx
It seems to me that you are not initialising the NSArray in the right method (viewDidLoad:).
Using XLData you don't have to care about where you set up the storage, reload the UITableView or add items to the NSArray (searchResultsArray) since it keeps track of the data (NSArray) and updates the UITableView accordingly and on the fly.
I know this question has been asked before, and I took a look at the answer to this question. However, I'm still confused as to how to implement reordering with a UITableView in a Core Data project. What I know is that I need to have a "displayOrder" attribute in my Entity to track the order of items, and I need to enumerate through all the objects in the fetched results and set their displayOrder attribute.
In the given code in the question I linked to, the table view delegate method calls a method like this [self FF_fetchResults];, and the code for that method is not given so its hard to tell what exactly it is.
Is there any sample code that demonstrates this? That would be simpler to look at than sharing large chunks of code.
Thanks
I'm not sure which part you are having trouble with (based on the comments)... but here is my suggestion. The displayOrder is just a simple attribute on a NSManagedObject class. If you can save a managed object, then you will be able finish this feature. Lets first take a simple NSManagedObject:
#interface RowObj : NSManagedObject
{
}
#property (nonatomic, retain) NSString *rowDescription;
#property (nonatomic, retain) NSNumber *displayOrder;
Next, we need to have local copy of the data being displayed in the tableview. I have read through the comments you have made and I'm not really sure if you are using the FetchedResultsController or not. My suggestion would be to start simple and just use a normal tableviewcontroller where you update the row data whenever a user changes the display order... then save the order when the user is done editing.
The interface for this tableviewcontoller would look like this:
#interface MyTableViewController : UITableViewController {
NSMutableArray *myTableViewData;
}
#property(nonatomic,retain) NSMutableArray *myTableViewData;
#end
Next, we need to load the the table view data in the viewWillAppear method:
- (void)viewWillAppear:(BOOL)animated {
myTableViewData = [helper getRowObjects]; // insert method call here to get the data
self.navigationItem.leftBarButtonItem = [self editButtonItem];
}
There are 2 things going on here... (I'll explain the editButtonItem later) the first is that we need to get our data from CoreData. When I have to do this I have some sort of helper(call it what you want) object do the work. A typical find method would look like this:
- (NSMutableArray*) getRowObjects{
NSFetchRequest *request = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"RowObj" inManagedObjectContext:[self managedObjectContext]];
[request setEntity:entity];
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"displayOrder" ascending:YES];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:sortDescriptor, nil];
[request setSortDescriptors:sortDescriptors];
[sortDescriptors release];
[sortDescriptor release];
NSError *error;
NSMutableArray *mutableFetchResults = [[managedObjectContext executeFetchRequest:request error:&error] mutableCopy];
if (mutableFetchResults == nil) {
// Handle the error.
}
[request release];
return mutableFetchResults;
}
Now that you have your data, you can now wait for the user to edit the table. That is where the [self editButtonItem] comes into play. This is a built in feature that returns a bar button item that toggles its title and associated state between Edit and Done. When the user hits that button, it will invoke the setEditing:animated: method:
To update the display order you need to override the setEditing method on the UITableViewController class. It should look something like this:
- (void)setEditing:(BOOL)editing animated:(BOOL)animated {
[super setEditing:editing animated:animated];
[myTableView setEditing:editing animated:animated];
if(!editing) {
int i = 0;
for(RowObj *row in myTableViewData) {
row.displayOrder = [NSNumber numberWithInt:i++];
}
[helper saveManagedObjectContext]; // basically calls [managedObjectContext save:&error];
}
}
We don't have to do anything when the user is in edit mode... we only want to save once they have pressed the "Done" button. When a user drags a row in your table you can update your display order by overriding the canMoveRowAtIndexPath and moveRowAtIndexPath methods:
- (BOOL)tableView:(UITableView *)tableView canMoveRowAtIndexPath:(NSIndexPath *)indexPath {
return true;
}
(void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath *)destinationIndexPath {
RowObj *row = [myTableViewData objectAtIndex:sourceIndexPath.row];
[myTableViewData removeObjectAtIndex:sourceIndexPath.row];
[myTableViewData insertObject:row atIndex:destinationIndexPath.row];
}
Again, the reason I don't update the displayOrder value here is because the user is still in edit mode... we don't know if the user is done editing AND they could even cancel what they've done by not hitting the "Done" button.
EDIT
If you want to delete a row you need to override tableView:commitEditingStyle:forRowAtIndexPath and do something like this:
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
if (editingStyle == UITableViewCellEditingStyleDelete) {
// Delete the managed object at the given index path.
RowObj *row = [myTableViewData objectAtIndex:indexPath.row];
[helper deleteRow:row];
// Update the array and table view.
[myTableViewData removeObjectAtIndex:indexPath.row];
[myTableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:YES];
}
}