UIViewController, UITableView subclassing - my solution feels dirty - ios

I have a a number of UIViewControllers in my project that implement the UITableViewDataSource and UITableViewDelegate protocols. In Interface Builder I have removed the UIView and replaced it with a subclassed UITableView. In my subclassed UITableView I set a custom backgroundView.
#interface FFTableView : UITableView
#end
#implementation FFTableView
- (id)initWithCoder:(NSCoder *)aDecoder {
self = [super initWithCoder:aDecoder]; // Required. setFrame will be called during this method.
self.backgroundView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"background.png"]];;
return self;
}
#end
This all works fine. I have half a dozen or so of these UIViewControllers that all have subclassed UITableViews and they all draw my background image. My background image is dark, so I need to draw my section headers so that they are visible. Googling I find How to change font color of the title in grouped type UITableView? and I implement viewForHeaderInSection in my subclas.
-(UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section
{
...
My viewForHeaderInSection is not called. Of course, when I thought about it for a moment, it makes sense. My UIViewControllers are the objects that implement UITableViewDataSource and UITableViewDelegate, and when I put viewForHeaderInSection in one of my UIViewControllers it works just fine.
But, I have half a dozen of these UIViewControllers, all are subclassed to different classes, implementing different functionality. So I decide to put a class inbetween my subclassed UIViewControllers and UIViewController. This way the common code of setting the appearance of the section headers will be in one spot, not in half a dozen spots.
So I write:
#interface FFViewController : UIViewController <UITableViewDataSource, UITableViewDelegate>
#end
and in here I implement viewForHeaderInSection:
-(UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section
{
NSString *sectionTitle = [self tableView:tableView titleForHeaderInSection:section];
if (sectionTitle == nil) {
return nil;
}
...
and change my other subclassed controllers to descend from FFViewController, here's one:
#interface FooDetailsViewController : FFViewController
It seems a little odd, having appearance code in 2 places, but it is better than having copies of the same coded scattered all over the place. In FooDetailsViewController I implement some of the Table Protocol methods, but not viewForHeaderInSection. I also get a warning on FFViewController since it doesn't implement all the protocols (which is on purpose, the child class, FooDetailsViewController in this example, fills out the protocol.)
So what's the question?
Not all of the other subclassed UIViewControllers respond to titleForHeaderInSection, so of course when I run I crash on those view controllers. So I try to see if titleForHeaderInSection is implemented:
-(UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section
{
if (![self respondsToSelector:#selector(titleForHeaderInSection)]) {
return nil;
}
NSString *sectionTitle = [self tableView:tableView titleForHeaderInSection:section];
if (sectionTitle == nil) {
return nil;
}
And respondsToSelector always returns false. So I can kill that section and just force all my subclasses to implement titleForHeaderInSection but that seems wrong.
What's a good way out of this situation? I'm already dislikeing the solution because:
The code has warnings. (But I can always do How to avoid "incomplete implementation" warning in partial base class to stop them)
My appearance code is in 2 separate classes
I need to implement titleForHeaderInSection in code that doesn't need it since it doesn't have sections headers
(Thanks for reading this far!)

In your respondsToSelector code you have given the wrong name for the selector you are testing for. It should be:
if (![self respondsToSelector:#selector(tableView:titleForHeaderInSection:)]) {
return nil;
}
If you make that change your test should work, and your subclasses can then optionally implement tableView:titleForHeaderInSection: or not.

Related

The nuances of UITableView

I have been using objective c for a few months now, using different foundation classes and generally playing around with the language.
In my own experience nothing has been more confusing than UITableView; Below is a bit of code that does not do much.
//the header file
#interface SLDataBankListTableViewController : UITableViewController <UITableViewDelegate, UITableViewDataSource>
#property (strong, nonatomic) IBOutlet UITableView *tableView;
#end
//implementation
#interface SLDataBankListTableViewController ()
#property (strong, nonatomic) SLDataSourceObject* dataSource;
#end
#implementation SLDataBankListTableViewController
- (void) viewWillAppear:(BOOL)animated {
[super viewWillAppear:YES];
_dataSource = [[SLDataSourceObject alloc] init];
}
#pragma mark - Table view data source
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return _dataSource.dataBankNames.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell* cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:#"reuse"];
cell.textLabel.text = [_dataSource.dataBankNames objectAtIndex:indexPath.row];
return cell;
}
#end
I have successfully used this class for over a dozen times and every time I make some stupid mistake like now. Finally I gathered some courage and decided to ask for help.
What are the subtle things, nuances of this class that I don't seem to grasp?
Edit: this is a part of a small program, other parts of it work fine, but the table displays nothing; Some changes have been recommended that are important but did not solve the problem.
It is a little hard to debug without known what is not working, but I see some things which might help you out.
UITableViewController has its own tableview but you seem to have another tableview wired up in a nib. Either use the UITableViewController tableview, or create your own, don't do both.
in cellForRowAtIndexPath you are creating a new cell every time instead of reusing the cells you have.
The delegate methods for the tableview can be called before viewWillAppear. You should create your datasource object earlier. I suggest viewDidLoad:. (Another reason viewWillAppear is a bad choice is that it can be call multiple times, and you can end up creating and destroying many datasource objects for no reason)
Hope that helps.
The big thing to remember about a table view is that it's a way for user to interact with an array of objects. The array is represented by a datasource, and the datasource methods describe what the view needs to know:
how many objects are in the array (called numberOfRowsInSection:)
how to display each one of the objects (called cellForRowAtIndexPath:)
To answer the latter question, the datasource must answer a view. That view's job -- like any view -- is to represent an object for the user. In row the table view uses a UITableViewCell.
The datasource array can be arbitrarily large, so directly mapping UITableViewCells to its elements can get arbitrarily expensive in memory terms. Rather than create a cell for every object in the array, the table view reuses cells.
When a cell scrolls off the top, the "new" one that appears at the bottom isn't new, it's the old cell handed back to the datasource to be reconfigured for the new row. To accomplish this, your datasource is expected to not allocate a new cell, as #JonRose correctly points out, but to ask the table view for a reused cell using dequeueReusableCellWithIdentifier.

tableview is connected to datasource and delegate - how

i am new to iOS, this may seem like a basic question. I have been working through this tutorial and I have no idea how the tableview is connected to the code. The sample project can be downloaded here.
I was my understanding that you need to extend UITableViewDelegate, UITableViewDataSource in the code, then in the storyboard you can drag from the tableview to them.
But what is perplexing is that the sample project does not extend UITableViewDelegate, UITableViewDataSource at all, therefore, how is the tableview in the story board connected to the code ?
Datasource is used to supply data and delegate is used to supply behaviour. UITableView asks your datasource every time it needs data to display. It provides a lot of flexibility for how you choose to represent your underlying data model. You simply define specific methods to use in order to get table information, and iOS can call them when it needs to know something like the number of rows in a section, or the content of a particular row.
You will probably implement your own delegate mechanism in the future. It is a great design pattern which handles interaction/data transfer between objects.
Because, the basic class adopts from UITableViewController
#interface WTTableViewController : UITableViewController
In your storyboard you just ctrl-drag from tableview to viewController and choose delegate and dataSource.
https://developer.apple.com/library/ios/documentation/UIKit/Reference/UITableViewDataSource_Protocol/ you should refer this it will help u alot
If you working with TableViewController, UITableViewDelegate and UITableViewDataSource are connected to table automatically. In case you are working with ViewControler which contains TableView, you have to add UITableViewDelegate and UITableViewDataSource like this: #interface myViewController : UIViewController <UITableViewDelegate, UITableViewDataSource>. In new versions of Xcode you can drag delegate and data source to table, and if you want to set delegate programmatically, you can add this two lines of codes:
[tableView setDelegate:self];
[tableView setDataSource:self];
or, equally:
tableview.delegate = self;
tableview.dataSource = self;
In viewcontroller.h file e
declare the delegate and datasource method.
UIViewController<UITableViewDataSource,UITableViewDelegate>
then
connect the delegate and datasource method with viewController so
in viewController.m file
- (void)viewDidLoad {
tableview.delegate=self;
tableview.dataSource=self;
}
In Objective-C/iOS you often implement things by having classes conform to protocols rather than subclassing classes. WTTableViewController conforms to the protocols
UITableViewDataSource: https://developer.apple.com/library/ios/documentation/UIKit/Reference/UITableViewDataSource_Protocol/
and
UITableViewDelegate: https://developer.apple.com/library/ios/documentation/UIKit/Reference/UITableViewDelegate_Protocol/
In MainStoryboard.storyboard you can see in the inspector to the right that the TableViewController has a custom class of WTTableViewController
The relevant methods in WTTableViewController.m:
#pragma mark - Table view data source
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return 2;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return 0;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"WeatherCell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
// Configure the cell...
return cell;
}
#pragma mark - Table view delegate
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
// Navigation logic may go here. Create and push another view controller.
}
Here's some general info about protocols: https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/ProgrammingWithObjectiveC/WorkingwithProtocols/WorkingwithProtocols.html
I suggest reading a beginners book or tutorial on iOS to pick up the overall design principles otherwise many things can be quite confusing.

Determine if UITableView has static cells or dynamic prototypes programmatically?

I'm writing an abstract UITableViewController class and I'd like to write something in viewDidLoad like
if (self.tableView.contentType == UITableViewContentTypeStaticCells) {
// Do something when table view has static cells
} else {
// Do something when table view has dynamic prototypes
}
But obviously there is no contentType on UITableView. Is there a way to determine programmatically whether the tableView's storyboard content is static or dynamic?
Just for the curious: [tableViewController valueForKey: #"staticDataSource"] will get you there, where tableViewController is a UITableViewController.
BUT(!) this might not pass the AppStore and may break without warning as it's not published API.
Update: It seems that checking if checking, if
self == self.tableView.dataSource
while self is a UITableViewController also gives you re requested result.
There's no build in way to distinguish between the two, but if you're more specific about what you're trying to achieve, we may be able to suggest alternative ways of accomplishing your goal.
my solution assumes the abstract UITableViewController class must expose a BOOL property
#property (assign, nonatomic) BOOL staticCells;
this property is valorised by the concrete classes, and the datasource methods implementation checks the property's existence condition like in this case:
- (UITableViewCell*) tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
if (!self.staticCells) {
...
}
else{
UITableViewCell* cell = [super tableView:tableView cellForRowAtIndexPath:indexPath];
[cell layoutIfNeeded];
return cell;
}
}
I suppose you were looking for a system framework property (or delegate method) to check the static behaviour but maybe this solution can be useful for someone

Overriding of Tableview Datasource/Delegate Methods

I have one parent class with one tableview.
That class is the delegate and datasource of that tableview as well.
Now I subclassed (derived) that class and made a child class.
I have one tableview in child class too.
Then I defined delegate and datasource functions in that child class, but it overrides parent class tableview data source/delegate methods.
But I want both of them to be separate.
However my Requirement is as Follows :
I want to retain a search bar and side button, on the top of all the viewControllers that search bar includes , a recent searches terms table underneath that.
So i thought of defining parent class for that and subclass other viewControllers from that class.
Am i doing it the right way ?
I assume you are talking about a view controller class. If I understood you right, then you are about to mess it up. Delegation is a way to avoid subclassing. Of course you can subclass the delegate - no problem. But you want a table view in the super class that owns a table in its view. And you want a subclass that has another table in its view plus the table that the superclass owns.
That is not impossible. But from your subclass' point of view, your subclass owns two table views. Even that is possible. Your view controller is the delegate of two tables (regardless of where in the view hierarchy they are declared and instanciated). When you now override the delegate and data source methods theny your subclass must either:
Determine which table it is dealing with/being called from. And then serve both tables appropriately.
Determine wich table it is dealing with/being called from. And then serve "its own" table appropriately and calls [super sameMehtod:withSamePamaters] to ensure that the superclas can still provide the data and server as delegate.
Which of both is smarter depends on the context and what you are about to achieve in detail.
A way of determinnig which table's delegate was called can be done by tagging the table views (do not use 0 as tag) or by comparing the tableView parameter of the delegate method with the corresponding properties (IBOutlets in this case). (In other cases you can compare the sender parameter with the IBOutlets. But tagging is probably easier to understand when reading the code later.)
Let's look at an example of the UITableViewDataSourceDelegat:
Your superclass implements:
#interface MySuperTableViewController:UITableViewController <UITableViewDelegate>
// There will be something in here.
// But it inherits self.tableView from UITableViewController anyway. We leave it with that.
#end
#implementation MySuperTableViewController
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
// This method creates or re-uses a cell object and sets its properties accordingly.
}
#end
And your subclass:
#interface MySubTableViewController : MySuperTableViewController // no need to declare the delegate here, it is inherited anyway
#property (weak, nonatomic) IBOutlet UITableView *mySecondTableView; // self.table will be used by the superclass already.
#end
#implementation MySubTableViewController
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
if (tableView == self.table) { // This call refers to the one talbe that is managed by super
return [super tableView:tableView cellForRowAtIndexPath:indexPath];
}
// This method now creates or re-uses a cell object and sets its properties accordingly.
// You may want to check wether tableView == self.mySecondTableView etc.
}
#end
(This comes from scratch, not syntax checked etc. Do not expect this to run properly right away :)
BUT ... please re-consider your class structure. I am afraid you are getting lost in some rather unlogical class hierarchy. There is nothing wrong with having two talbes managed by a common view controller even without this subclassing-thing. And there is nothing wrong with using multiple tables in a view where each of the tables has its own delegate (can be a view controller). Since iOS 5 (or was it introduces with 6) we can use the UIContainerView for that purpose and nicely build it up in IB/storyboard.
try this,
ViewController.h
IBOutlet UITableView *firstTable;
IBOutlet UITableView *secondTable;
ViewController.m
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
// Return the number of sections.
if (tableView == firstTable) {
return 1;
}
else if(tableView == secondTable)
{
return 1;
}
return 0;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
// Return the number of rows in the section.
if (tableView == firstTable) {
return [arrItems count];
} else if(tableView == secondTable)
{
return [arrData count];
}
return 0;
}
etc etc ....

Implementing a custom table section header via Storyboard and new Xib

Here is what I have done:
I created a custom xib file that has a small UIView used for a custom table section header.
I classed the custom xib file.
I want to add this to a tableView as the header. I have looked at a few resources, but they seem to be either outdated or missing information.
Looking in the documentation, I see a reference to adding a custom header with the following instructions:
To make the table view aware of your header or footer view, you need to register it. You do this using the registerNib:forCellReuseIdentifier: or registerClass:forCellReuseIdentifier: method of UITableView.
When I added a tableView to my storyboard view, it was easy to assign it a reuse identifier within XCode. I was even able to create a custom cell xib file and it also had a spot for a reuse identifier within XCode.
When I created the custom UIView for the section header, it did not have an entry for reuse identifier. Without this, I have no idea how to use registerNib:forCellReuseIdentifier.
More information:
I have a storyboard scene that has a tableView inside. The tableView is of a custom class that is linked and the tableView object has an outlet in the parent view's ViewController file.
The parent ViewController is both the UITableViewDataSourceDelegate and UITableViewDelegate. Again, I was able to implement the custom cells with no issue. I can't even modify the header in any way besides the title.
I tried calling the method [[self tableHeaderView] setBackgroundColor:[UIColor clearColor]]; from the custom tableView class and nothing happens. I tried using this method in the parent ViewController class by using the outlet name like so:
[[self.tableOutlet tableHeaderView] setBackgroundColor:[UIColor clearColor]];
Any help would be greatly appreciated.
EDIT: (Can't change background to transparent)
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section {
HeaderView *headerView = [self.TableView dequeueReusableHeaderFooterViewWithIdentifier:#"tableHeader"];
// Set Background color
[[headerView contentView] setBackgroundColor:[UIColor clearColor]];
// Set Text
headerView.headerLabel.text = [self.sectionArray objectAtIndex:section];
return headerView;
}
You don't need to set the identifier in the xib -- you just need to use the same identifier when you register, and when you dequeue the header view. In the viewDidLoad method, I registered the view like this:
[self.tableView registerNib:[UINib nibWithNibName:#"Header1" bundle:nil] forHeaderFooterViewReuseIdentifier:#"header1"];
Then, in the delegate methods:
-(UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section {
UIView *headerView = [self.tableView dequeueReusableHeaderFooterViewWithIdentifier:#"header1"];
return headerView;
}
-(CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section {
return 100;
}
On the problem with background color (Unless you want transparent):
You can create an UIView which occupies the whole view then change the background color of that.
If you don't want others to know what's happening, you can overwrite the backgroundColor property:
//interface
#property (copy, nonatomic) UIColor *backgroundColor;
//implementation
#dynamic backgroundColor;
- (void)setBackgroundColor:(UIColor *)backgroundColor {
//self.viewBackground is the created view
[self.viewBackground setBackgroundColor:backgroundColor];
}
- (UIColor *)backgroundColor {
return self.viewBackground.backgroundColor;
}

Resources