views:

1052

answers:

2

It bugs me to death that my viewcontroller, which happens to be a tableViewController, knows without being told that its property that is an NSArray or an NSDictionary holds the data that is to be loaded into the table for display.

Seems like I should explicitly say something like:

[self.tableView useData:self.MyArray];

I want to have more than one array inside my tableViewController and switch between one and the other programmatically.

I notice that when a tableViewController makes use of a searchViewController, you can do this:

if (tableView == self.searchDisplayController.searchResultsTableView) {

I have even been able to do this:

self.tableView =  self.searchDisplayController.searchResultsTableView;
[self.tableView reloadData];

But nowhere can I find how to set self.tableView back to the main datasource!

+2  A: 

A table view's controller doesn't "know without being told" anything-- it doesn't inherently have a property like you mention that data comes from. You supply that data, one cell at a time, generally in your view controller subclass.

Typically your table view controller object is both the table view's delegate and the table view's data source delegate. From the Apple docs:

A UITableView object must have a delegate and a data source. Following the Model-View-Controller design pattern, the data source mediates between the application’s data model (that is, its model objects) and the table view; the delegate, on the other hand, manages the appearance and behavior of the table view. The data source and the delegate are often (but not necessarily) the same object, and that object is frequently a custom subclass of UITableViewController.

The table view doesn't take in an array or dictionary and pull data from it; it asks you in your data source what each cell should look like. You just implement this method:

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

And return whatever contents in a cell that you want for the row you're being asked about. You can put logic in there to mix/match/pull data from wherever you want.

Could your confusion come from a soup of sample code maybe that's unclear about what's going on? I'd recommend building a table view from scratch to see how this works-- it's easy to do this by adding a new class to your project, you can select a UITableViewController subclass from inside XCode in the "new" wizard. It will prepopulate the .m file with all the relevant empty methods, including the above.


EDIT: Don't change the table view your view controller owns when doing a search. You're confusing the instance reference called "tableView", which your view controller owns, with the argument to the delegate method tableView:cellForRowAtIndexPath:, which is just getting passed in to tell you which table view is asking for a cell. When you have search set up in the normal way with the same viewcontroller being the delegate for both the default/content table and the search results, you could get called with either. See here for the docs on this.

quixoto
You say: "it asks you in your data source..." No. It asked me no such thing. I know how to use tableView:cellForRowAtIndexPath. And I have seen the logic in there that I quoted above:if (tableView == self.searchDisplayController.searchResultsTableView) {Where do I find how to set the tableView equal to anything? Or, what other things can I test for equivalency besides searchDisplayController.searchResultsTableView?
Scott Pendleton
Edited to add info re: search results. Scott: I understand that it can be an uphill battle to understand some of these designs. But you should note that you're much more likely to get useful responses and help if you don't write as if you're lashing out at your fellow Stack Overflow members. Honey, vinegar, etc.
quixoto
Good advice, thanks.
Scott Pendleton
+10  A: 

Okay, I understand your frustrations because the vast majority of iPhone instructional material do not pay sufficient attention to overall app design. They make a beeline for the eye candy interface and pay only lip service to way that the app should handle the data even though handling data is the entire purpose of the app in the first place!

The instructional materials do not spend enough time explaining the Model-View-Controller design pattern on which the entire iPhone/Cocoa API is based. You're having a hard time understanding anything because you keep trying to cram functionality into the wrong objects under the mistaken belief that the UI view is the core of the program as the instructional materials have led you to believe. Under this misapprehension, nothing makes sense, not even the Apple Documentation.

You need to step back and rethink. It is not the function of a view to decide what data to display and when to display it. It is not the function of the table view controller to hold, manage or store the app's data. Those functions properly belong to the data model object (which you've possibly never heard of.) You're having trouble because you are trying to split the data model task across the view and the view controller were they do not belong.

Apparently, your app doesn't even have a data model because you are holding the table's data as properties of the tableview controller. Although you often see this in simplistic tutorial examples, it is bad design which will collapse under the complexity of any but the most trivial apps.

Instead, your data should be stored in and managed in its own custom object. This is the data model. In your case, it sounds like you have data spread across two arrays so you would create a data model object something like this:

@interface MyDataModel : NSObject {
@protected
    NSArray *arrayOne;
    NSArray *arrayTwo;
@public
    NSArray *currentlyUsedArray;

}
@property(nonatomic, retain)  NSArray *currentlyUsedArray;

-(void) switchToArrayOne;
-(void) switchToArrayTwo;
-(void) toggleUsedArray;

@end

#import "MyDataModel.h"

@interface MyDataModel ()
@property(nonatomic, retain)  NSArray *arrayOne;
@property(nonatomic, retain)  NSArray *arrayTwo;

@end


@implementation MyDataModel

- (id) init{
    if (self=[super init]) {
        self.arrayOne=//... initialize array from some source
        self.arrayTwo=//... initialize array from some source
        self.currentlyUsedArray=self.arrayOne; //whatever default you want
    }
}

-(void) switchToArrayOne{
    self.currentlyUsedArray=self.arrayOne;
}

-(void) switchToArrayTwo{
    self.currentlyUsedArray=self.arrayTwo;
}

- (void) toggleUsedArray{
    if (self.currentlyUsedArray==self.arrayOne) {
        self.currentlyUsedArray=self.arrayTwo;
    }else {
        self.currentlyUsedArray=self.arrayOne;
    }
}

(Notice that the actual data is encapsulated and that other objects can only access the currentlyUsedArray. The data model decides which data to provide based on the internal state of the data.)

This data model object should be in a universally accessible location. The best method is to make it a singleton but the quick and dirty method is to park it as an attribute of the app delegate.

So in you tableview controller you would have a property:

MyDataModel *theDataModel;
@property (nonatomic, retain) MyDataModel *theDataModel;

then in the implementation

@synthesize theDataModel;

-(MyDataModel *) theDataModel; {
    if (theDataModel; !=nil) {
        return theDataModel; ;
    }
    id appDelegate=[[UIApplication sharedApplication] delegate];
    self.theDataModel=appDelegate.theDataModelProperty;
    return theDataModel;
}

Then in your tableview datasource method:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
    ...
    cell.textLabel.text=[self.theDataModel.currentlyUsedArray objectAtIndex:indexPath.row];
    return cell;
}

If some event anywhere in the app requires you to switch arrays, you just call up the data model object from the app delegate and send it the appropriate switch array message.

id appDelegate=[[UIApplication sharedApplication] delegate];
[appDelegate.theDataModelProperty toggleUsedArray];

Now all subsequent data operations, whether in that particular table view or some other completely unrelated view, will use the data form the proper array.

Why go through all this trouble? It makes the application modular. You can easily add on different views each of which display the data in a different manner without having to rewrite your data management every single time. You can use the data model to manage data that will be displayed in a table, in a webview or on the command line. You can even easily move the data model to an entirely different app.

This modularity makes the management of large complex apps so much easier. You have only one object that manipulates and controls the data. You don't have to worry that some minor error in some rarely used code segment will trash the entire app. You can plugin views easily or remove them easily without breaking the app.

This is of course a trivial example but it shows good practice.

However, you may ask, how does this solve the problem of the tableview knowing what data to load and when to load it? Simple, it doesn't. It is not the job of the tableview to know what data to load or when to load. The data model handles the what-data and tableview controller handles the when. (You can even have the data model issue notifications when it is updated e.g. for a url. then the view controller can register for the notification and call reloadData whenever the data model changes.)

By ruthlessly compartmentalizing and encapsulating functionality in MVC, you create complex apps from simple, reusable components that are easy to maintain and debug.

It's really to bad most instructional materials only pay lip service to this utterly critical concept.

TechZen
Excellent! Your careful, clear writing conveys your grasp of this subject in a way that I truly understand. Much obliged. You are correct that the sample code tosses the data into the app delegate, which then passes it to the tableViewController, or in some cases the sample data are generated in the tableViewController's viewDidLoad event handler. Now I know that that is bad practice. I also didn't grasp that tableView:cellForRowAtIndexPath: was simply populating the table cells with data that I had in fact specified. I do not know what you mean by a "singleton". I will look it up.
Scott Pendleton
Problem: the first array is longer than the second. When I switch arrays and call [self.tableView reloadData], it then calls tableView:cellForRowAtIndexPath: once for each item in the longer array, which causes an out-of-bounds error. This proves that self.tableView has somehow made note of the length of original array. But the number is no longer valid! You say it's not the tableView's job to know what data to load. Well, [self.tableView reloadData] is calling tableView:cellForRowAtIndex:, and in order to know how many times to call it, it MUST know about its data source! See the problem?
Scott Pendleton
are you using the method numberOfRowsInSection: and numberOfSectionsInTableView: ? That is meant to return how many rows in the table (and section). In this case numberOfRowsInSection: should just return [self.theDataModel.currentlyUsedArray count]; This def gets called each time you call [self.table reloadData]; but not sure what would happen if you don't set this.
Rudiger
That's it! That was the missing piece of the puzzle. So the tableViewController really doesn't know or remember what data you feed it. You just use two methods, one to give it the number of rows, and one to give it the data row by row, and voila!Many thanks to all.
Scott Pendleton
Don't take offence to this but I feel you will continue to run into trouble. You are coding using the concepts of other coding languages and getting stuck because you are trying to apply them to iPhone dev. I went through the exact thing and became very frustrated. I would recommend not doing examples that you find on the net but rather having a look at Stamfords iPhone lectures available on iTunes. I personally think that the concepts of programming on the iPhone are very different to other languages and are invaluable once you know them, you can pick them up in a day and make your life easy
Rudiger
If I create a form in Microsoft Access to display rows from one table, I can programmatically reset the form to use a different table. Now I understand that I can do exactly that with a UITableView. The difference is that in Access I would change the form's recordsource property, and the form would then populate the rows and know the number of rows. With the UITableView, you just say "load so many rows of data, and here they are." Not quite equivalent, but close enough. My problem was not realizing that there was no property to set, but instead two methods to use.
Scott Pendleton
In addition, I was confused by the fact that the searchDisplayController has its own tableView that it puts on top of the tableViewController's tableView. Once I figured out that I could control the data added to either tableView, it was then just a question of figuring out which will display under which circumstances, and sending my data to the one that is actually visible.
Scott Pendleton
TechZen plays teacher, counselor and coach with extreme patience. +1 towards a Reversal.
quixoto
There are a lot more than just two methods for UITableView including the height of the row, number of sections and title for section. Hold down command key and double click the word UITableView in your code and you will see how many other delegate methods there are. There are many other objects that work the same way like NSURLConnection which you will probably have problems with again somewhere down the track
Rudiger
Thanks SO MUCH. This is BY FAR the best answer that I've seen on here so far!!!!
K-RAN