0

I'm creating a UITableView in which product information can be added. In each row, the user can add information about a product, and, obviously, the user can set the number of rows himself.

the user can add or remove one row a time by tapping either the "add row" or "remove row" button in the NavigationBar. this is how it works:

- (void)viewDidLoad
{
    [super viewDidLoad];

    tableRows = [NSNumber numberWithInt:12];

}

-(void) addRow
{
    NSNumber *addRow =[NSNumber numberWithInt:1];
    tableRows= [NSNumber numberWithInt:(tableRows.intValue + addRow.intValue)];
    [self.tableView reloadData];

    NSLog(@"%@", tableRows);
}

-(void) removeRow
{
    NSNumber *addRow =[NSNumber numberWithInt:1];
    tableRows= [NSNumber numberWithInt:(tableRows.intValue - addRow.intValue)];
    [self.tableView reloadData];

    NSLog(@"%@", tableRows);
}
- (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 tableRows.intValue;
}

    - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {

        static NSString *CustomCellIdentifier = @"CustomCellIdentifier ";
        CustomCell *cell = (CustomCell *)[tableView dequeueReusableCellWithIdentifier: CustomCellIdentifier];
        if (cell == nil) {
            NSArray *nib = [[NSBundle mainBundle] loadNibNamed:@"CustomCell"
                                                         owner:self options:nil];
            for (id oneObject in nib) if ([oneObject isKindOfClass:[CustomCell class]])
                cell = (CustomCell *)oneObject;
            cell.selectionStyle = UITableViewCellSelectionStyleNone;

        }
        NSUInteger *row = [indexPath row];

        return cell;
    }

The editing works perfect but when I add or remove a row, the text I inserted in the textfields of my tableview disappears.

does anybody know how to prevent this?

rmaddy
  • 314,917
  • 42
  • 532
  • 579
Joris416
  • 4,751
  • 5
  • 32
  • 59

2 Answers2

3

A couple things: The table view doesn't have responsibility to remember what's in each of the cells. It throws away cells as the scroll away and asks the datasource to initialize them again via cellForRowAtIndexPath. Reloaddata - which you use in your add/remove methods - will cause the table to refresh all of the visible cells. Don't expect anything to appear in your table that isn't setup in cellForRowAtIndexPath.

Next, your "model" for this table is an NSNumber "tableRows" indicating the number of rows. This is an insufficient model for a table view. Replace it with an NSMutableArray. At the very least, this array should contain strings representing the state of each text field. (and it might need even more elaborate objects, but start with strings).

With that, your view controller class will look more like this...

// this is your table's model
@property (nonatomic, strong) NSMutableArray *rows;

// in init for the class
_rows = [NSMutableArray array];

// somewhere else, put some data in it
[self.rows addObject:@"Foo"];
[self.rows addObject:@"Bar"];

Now your datasource methods:

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    // Return the number of rows in the section.
    return self.rows.count;
}

Then, in cellForRowAtIndexPath:

NSUInteger *row = [indexPath row];   // as you have it
NSString *rowText = self.rows[row];  // this is new syntax, the old way is [self.rows objectAtIndex:row];

// your CustomCell needs to provide a way to get at the textField it contains
// it might do this with an outlet or viewWithTag...
cell.myTextField.text = rowText;
return cell;

Finally, text fields in the cells pose a particular challenge. How to save their current state when the view isn't scrolling. This problem has been asked and answered multiply in SO (here, for example). In a nutshell, the most common solution is to make the view controller the delegate of the text fields in the cells. Then, on textFieldDidEndEditing, save the value of the textField in your model like this...

- (void)textFieldDidEndEditing:(UITextField *)textField {

    NSIndexPath *indexPath = [self indexPathOfCellWithSubview:textField];
    self.rows[indexPath.row] = textField.text;
}

// I think this is the best way to get the index path of a cell, given some subview it contains
- (NSIndexPath *)indexPathOfCellWithSubview:(UIView *)view {

    while (view && ![view isKindOfClass:[UITableViewCell self]]) {
        view = view.superview;
    }
    return [self.tableView indexPathForCell:(UITableViewCell *)view];
}

EDIT Say there's more to the model than just a single string. This is where you would apply a custom subclass of NSObject.

// MyModel.h
@interface MyModel : NSObject

@property (strong, nonatomic) NSString *itemName;
@property (assign, nonatomic) CGFloat price;
@property (strong, nonatomic) NSString *imageFileName;
@property (strong, nonatomic) UIImage *image;

- (id)initWithItemName:(NSString *)itemName price:(CGFloat)price imageFileName:(NSString *)imageFileName;

- (NSString *)stringPrice;
- (void)setStringPrice:(NSString *)stringPrice;

@end

// MyModel.m
@implementation MyModel

- (id)initWithItemName:(NSString *)itemName price:(CGFloat)price imageFileName:(NSString *)imageFileName {

    self = [self init];
    if (self) {
        _itemName = itemName;
        _price = price;
        _imageFileName = imageFileName;
    }
    return self;
}

// override the image getter to "lazily" create and cache the image
// if the images are on the web, this will require a slighly more elaborate method
// employing NSURLConnection.
- (UIImage *)image {

    if (!_image) {
        _image = [UIImage imageNamed:self.imageFileName];
    }
    return _image;
}

// added these to show you how you can conveniently encapsulate other
// behavior, like type conversion or validation, though, real ones of these
// would probably use NSNumberFormatter
- (NSString *)stringPrice {

    return [NSString stringWithFormat: @"%.2f", self.price];
}

- (void)setStringPrice:(NSString *)stringPrice {

    self.price = [stringPrice floatValue];
}

Now you can create one like this and add it to your table. (Be sure to #import "MyModel.h")

[self.rows addObject:[[MyModel alloc] initWithItemName:@"Toaster" price:39.95 imageFileName:@"toaster.png"]];

The view controller containing the table stays more or less the same (when you change one class a lot and change a closely related class very little, it tells you that your OO design is probably pretty good). For the fancy model replacing the string, we need to change cellForRowAtIndexPath...

NSUInteger *row = [indexPath row];
MyModel *myModel = self.rows[row];

cell.itemNameTextField.text = myModel.itemName;
cell.priceTextField.text = [myModel stringPrice];
cell.imageView.image = myModel.image;

// additional OO idea: teach your cell how to configure itself and move the foregoing code there
// [cell configureWithModel:myModel];

return cell;

ANOTHER EDIT: We can teach this model how to post itself to a remote web service as follows:

- (void)post {
    NSString *hostStr = @"http://myhost/create_product.php";
    NSURL *url = [NSURL URLWithString:hostStr];

    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
    request.HTTPMethod = @"POST";

    NSString *post =[NSString stringWithFormat:@"item_name=%@&price=%@",self.itemName, [self stringPrice];
    NSString *postEscaped = [post stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
    NSData *postData = [postEscaped dataUsingEncoding:NSUTF8StringEncoding allowLossyConversion:YES];

    [request setHTTPBody:postData];
    [request setValue:@"application/x-www-form-urlencoded charset=utf-8" forHTTPHeaderField:@"Content-Type"];

    [NSURLConnection sendAsynchronousRequest:request
                                       queue:[NSOperationQueue mainQueue]
                           completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {

                           if (!error) {
                               NSString *string = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
                               NSLog(@"response %@", string);
                           } else {
                               NSLog(@"error %@", error);
                           }
                       }];
}

Declare this method in the .h, add other fields to the post as you see fit (e.g. the image file name, etc.)

In your view controller, pick out the action that means the user wants to commit the new row (maybe it's when the text field is finished editing?), and add this...

// text field finished editing
MyModel *myModel = self.rows[indexPath.row];
myModel.itemName = textField.text;
[myModel post];

Since the image will probably come from your remote service, you'll want to change the lazy loading image getter I added earlier. The right way to load this image is asynchronously, but doing so complicates the interaction with the table view too much to discuss here. Refer to apple docs or this SO post to learn more about that. In the meantime, here's the quick -- but basically wrong -- way to get the image synchronously...

   - (UIImage *)image {

    if (!_image) {
        // note - now the file name must be of the form @"http://host/path/filename.png"
        NSURL *url = [NSURL URLWithString:self.imageFileName
        _image = [UIImage imageWithContentsOfURL:url];
    }
    return _image;
}
Community
  • 1
  • 1
danh
  • 62,181
  • 10
  • 95
  • 136
  • Thank you! i'm trying to get this to work now. one question: my tableViewCells are containing more than one textfield. Each cell contains 2 textfields (the product name and price), one textview (to make a short product description) and a UIImage. Due to this, I'll probably have to make a 2-dimensional mutable array, right? if so, how do i edit your piece of code to get it to work with a 2-dimensional array? – Joris416 May 26 '13 at 10:05
  • Sure, will edit now. Your situation calls for a custom subclass of NSObject, not any further dimensions in the array. – danh May 26 '13 at 13:53
  • Wow! that's some pretty impressive code, at least to me as a beginner. i'm applying it now to test it. I was thinking of a 2-dimensional array at first because all the products entered, will be sent to a mysql database through php. I'll try to figure out how that works with this later on, but thanks a lot! – Joris416 May 26 '13 at 15:42
  • do you know if it's possible to access each value separately to send it to mysql? if not, i probably can't use this. for now it works, so that's the last thing i need to now about your answer ;) – Joris416 May 26 '13 at 19:16
  • Can you explain what you mean by "access each value separately"? Now that we have a good encapsulated model object, we can teach it how to post itself to a remote web service. Is that what you're thinking? – danh May 26 '13 at 19:23
  • I'll give some background information. I'm building 2 apps: this one is for shopkeepers and the other one for consumers. I want to allow shopkeepers to add all their products in this app and, obviously, consumers to look up information about those products. therefore, I need this app to send all the entered productInformation (productName, productDescription, productPrice and productImage) through php to a MySql database. i'll make sure the picture is in a separate directory on my website in order to prevent the database from getting slow. is this clear enough? – Joris416 May 26 '13 at 19:40
  • i'm open to any way of doing this, but I want you to know i'm a beginner and I'm using these projects to get familiar with objective-c and the iOS platform. I still have to learn a lot.. – Joris416 May 26 '13 at 19:42
  • I gotcha. Will add some code in a min to demonstrate how to post that model to a web service. You'll also want the images to be loaded remotely, so that's a change to the code I added already. Finally, I hope I've answered the original question adequately. It probably harms this SO post to add too much info to it unrelated to text fields in the table. – danh May 26 '13 at 20:03
  • If i could only give you 10 upvotes for your time and effort... Ill insert & test this tomorrow. (posting this from my ipad) If it works, what i expect, ill check the "answered" box. Thank you so much! – Joris416 May 26 '13 at 22:26
  • Sure thing. Let me know if you have an issue related to the subject matter here. Happy to help. – danh May 27 '13 at 16:22
  • let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/30699/discussion-between-imagine-digital-and-danh) – Joris416 May 27 '13 at 16:36
  • @dahn, I have a question about you custom subclass. I need to allow the user to add more products at the time, and therefore I created an NSMutableArray. each time the user taps the "add product" button, one instance of your Model gets added to that array. everytime the user stops editing a textfield, the text gets places into the model at the right indexpath. so good so far. Now, when the user taps "safe and go back", my leftBarButtonItem, I need to send all products to my database and deny this viewController. My problem is: How do I access for example `model.itemName` when its in an array? – Joris416 Jun 27 '13 at 10:28
0

It would be helpful to see your code for cellForRowAtIndexPath, we need to know more about the model you intend to store data in.

When you delete a row from the table, that cell is thrown out, and the tableview will not remember the contents automatically. You must save the changes in a model object as they occur, and then use that to populate the cell's contents when returning a cell from cellForRowAtIndexPath.

Nick
  • 2,361
  • 16
  • 27
  • I'm using customCells, I will post my cellForRowAtIndexpath soon. I do not want to remember the content of removed cells. Let's say I have 2 rows and I only add text in the first. when I press "add row", my text is gone. same occurs when I press "remove row". – Joris416 May 25 '13 at 17:29
  • I posted it!, I hope you can do something with this – Joris416 May 25 '13 at 17:42