Here is a simplified version of my controller. I'm creating a table view and I initialize this controller with a core data query that returns an array of around 300 objects.
I simplified the code here by hardcoding the height of each cell to 100.0f. Instead of only loading the first 5-6 cells that are actually visible on the screen on first load, the call to heightForRowAtIndexPath is actually done 300 times (for all cells), which makes the UI slow to load.
Any idea what I might be doing wrong here?
#interface PortsViewController () <UITableViewDataSource, UITableViewDelegate>
#property (nonatomic, strong) UITableView *tableView;
#end
#implementation PortsViewController
#pragma mark - setters
- (void)setPorts:(NSArray *)ports
{
_ports = ports;
}
#pragma mark - Controller Methods
- (id)init
{
self = [super init];
self.screenName = kPortsListPage;
NSArray *ports = [Port MR_findAllSortedBy:#"name" ascending:TRUE];
[self setPorts:ports];
return self;
}
- (void)loadView {
self.view = [[UIView alloc] initWithFrame:[UIScreen mainScreen].bounds];
[self loadTableView];
}
- (void)viewDidLoad
{
[super viewDidLoad];
self.tableView.delegate = self;
self.tableView.dataSource = self;
self.title = #"Ports";
}
#pragma mark - Load Helpers
-(void)loadTableView {
self.tableView = [[UITableView alloc] initWithFrame:self.view.bounds style:UITableViewStylePlain];
self.tableView.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth;
[self.view addSubview:self.tableView];
}
#pragma mark - UITableViewDataSource Methods
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return [self.ports count];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
PortTableViewCell* cell = [table dequeueReusableCellWithIdentifier:#"port"];
if (!cell) {
PortTableViewCell* cell = [[PortTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:#"port"];
}
cell.port = self.ports[indexPath.row];
return cell;
}
#pragma mark - UITableViewDelegate Methods
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
NSLog(#"here");
return 100.0f;
}
#end
The table view needs to know it's total height. if you implement heightForRowAtIndexPath: it will be called for every row so the table know how tall it is.
If every row has the same height, simply set the rowHeight property on the table view instead of implementing the heightForRowAtIndexPath delegate method.
If each row can have a different height, then there is little you can other than making your implementation of heightForRowAtIndexPath as efficient as possible.
As of iOS 7, there is now the estimatedRowHeight property on UITableView. Using that should help improve things.
Related
I have a UITableView in my app and I'm trying to pull it's delegate methods into a separate UITableViewDelegate. This is what the code looks like:
RestaurantViewDelegate *delegate = [[RestaurantViewDelegate alloc] initWithRestaurant:self.restaurant andRecommended:self.recommended];
self.tableView = [[UITableView alloc] initWithFrame:CGRectMake(0, 235.0f, self.view.frame.size.width, self.view.frame.size.height-235)];
self.tableView.delegate = delegate;
self.tableView.dataSource = delegate;
[self.view addSubview:self.tableView];
And this is what RestaurantViewDelegate looks like:
// RestaurantViewDelegate.h
#interface RestaurantViewDelegate : NSObject <UITableViewDelegate>
#property (nonatomic, strong) NSArray *recommendations;
#property (nonatomic, strong) Restaurant *restaurant;
- (id)initWith Restaurant:(Restaurant *)restaurant andRecommended:(NSArray *)recommendations;
#end
and
// RestaurantViewDelegate.m
#implementation RestaurantViewDelegate
#synthesize recommendations = _recommendations;
#synthesize restaurant = _restaurant;
- (id)initWith Restaurant:(Restaurant *)restaurant andRecommended:(NSArray *)recommendations {
self = [super init];
if ( self != nil ) {
_recommendations = recommendations;
_restaurant = restaurant;
}
return self;
}
#pragma mark - UITableViewDataSource
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
NSLog(#"Recommendations: %d", [_recommendations count]);
return [_recommendations count];
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
return 48.0f;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *MyIdentifier = #"MyIdentifier";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:MyIdentifier];
if (cell == nil)
{
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:MyIdentifier];
}
return cell;
}
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return 1;
}
#end
However, when I run my app and click on a cell, all cells disappear. I really don't know what's causing this. Any ideas as to what I'm doing wrong?
This is a very interesting problem. Remember that in ARC (Automatic reference counting ), an object will be retained only as long as a strong reference to it is maintained. Remember, the 'delegate's are always weak, and in your case it means, once you come out of the scope, where you create the delegate object and setup the table view, there will no longer any delegate object retained. This is the reason you might not see anything happening when you try to reload the table view. Make the delegate object RestaurantViewDelegate a member of your controller. And check..
I am trying to add a TableView as a subview in my RootViewController.The TableView will come from another ViewController(TableViewGeneratorController) instance method.
So,What is the best way to do this?
I have created a TableViewGeneratorController it works fine as a standalone app. Then from my RootViewController I have created one instance of the TableViewGeneratorController and trying to call the instance method prepareField,which will return the TableView. I got the TableView but
numberOfRowsInSection and cellForRowAtIndexPath is not getting called.
TableViewGeneratorController.h
#import <UIKit/UIKit.h>
#interface TableViewGeneratorController:UIViewController<UITableViewDataSource,UITableViewDelegate>
{
UITableView *tableView;
}
#property(strong,nonatomic)UITableView *generatedTbleView;
- (UITableView *)prepareField;
#end
TableViewGeneratorController.m
#import "TableViewGeneratorController.h"
#import "RootViewController.h"
#interface TableViewGeneratorController (){
}
#end
#implementation TableViewGeneratorController
- (void)viewDidLoad {
[super viewDidLoad];
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
- (UITableView *)prepareField
{
tableView = [[UITableView alloc]initWithFrame:CGRectMake(10, 80, 300, 500)];
tableView.dataSource = self;
tableView.delegate = self;
tableView.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth;
[tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:#"Cell"];
[tableView reloadData];
return tableView;
}
#pragma mark - Table view data source
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return 10;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
static NSString *cellIndentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIndentifier];
if (cell == nil){
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIndentifier];
}
cell.textLabel.text = #"Yes";
return cell;
}
#end
RootViewController.m
Here I am trying to add the TableView as a subview.
TableViewGeneratorController *tableViewGeneratorController = [[TableViewGeneratorController alloc]initWithNibName:#"TableViewGeneratorController" bundle:nil];
UITableView *tv = [tableViewGeneratorController prepareField];
[self.view addSubview: tv];
What is the problem going on?
Thanks
What you are doing is not really the proper way.
The immediate issue is that the TableViewGeneratorController instance that you create goes out of scope and is deallocated. This leaves the table view with not existing data source or delegate. A simple workaround is to assign the TableViewGeneratorController instance to an instance variable instead of a local variable.
But the proper solution is to embed the TableViewGeneratorController as a child controller of the root view controller.
Change TableViewGeneratorController to be a UITableViewController and get rid of the prepareField method.
Then when you create the TableViewGeneratorController, you add it as a child controller. See the docs for UIViewController for details.
in my application i am using storyboards. In the storyboard i have used the default view controller for login and created another view controller name Inventory. In the new view controller i am using a table view. The table is displaying but the delegate and datasource methods are not being called. I have tried many solutions but i dint understand the problem.The code i used is:
#import <UIKit/UIKit.h>
#interface InventoryViewController : UIViewController <UITableViewDataSource, UITableViewDelegate>
#end
the implementation file is:
#import "InventoryViewController.h"
#import "customCell.h"
#interface InventoryViewController ()
#end
#implementation InventoryViewController
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
if (self) {
// Custom initialization
}
return self;
}
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view.
self.view.backgroundColor = [UIColor blueColor];
UITableView *table = [[UITableView alloc] initWithFrame:CGRectMake(0, 110, 320, 350)];
table.delegate = self;
table.dataSource = self;
[self.view addSubview:table];
[table relaodData];
}
- (IBAction)Logout:(id)sender {
UIStoryboard *storyboard = [UIStoryboard storyboardWithName:#"Main" bundle:nil];
UIViewController *vc = [storyboard instantiateViewControllerWithIdentifier:#"Login"];
[self.navigationController pushViewController:vc animated:YES];
}
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
#pragma mark - UITableViewDelegate Methods
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return 1; }
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
NSLog(#"hello");
return 5;
}
- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
customCell *cell = [tableView dequeueReusableCellWithIdentifier:#"customcell"];
if (cell == nil)
{
cell = [[customCell alloc] initWithStyle:UITableViewCellStyleDefault
reuseIdentifier:#"customcell"];
}
// Here we use the provided setImageWithURL: method to load the web image
// Ensure you use a placeholder image otherwise cells will be initialized with no image
cell.item_image.image = [UIImage imageNamed:#"ipad_strip_logo.png"];
cell.type.text = #"electronics";
cell.name.text = [NSString stringWithFormat:#"my object %d",indexPath.row];
return cell;
}
#end
You need to apply -reloadData: method after setting deledate, datasource and datasource array.
reloadData
Reloads the rows and sections of the receiver.
- (void)reloadData
Discussion
Call this method to reload all the data that is used to construct the table, including cells, section headers and footers, index arrays, and so on. For efficiency, the table view redisplays only those rows that are visible. It adjusts offsets if the table shrinks as a result of the reload. The table view's delegate or data source calls this method when it wants the table view to completely reload its data. It should not be called in the methods that insert or delete rows, especially within an animation block implemented with calls to beginUpdates and endUpdates
Go through this doc https://developer.apple.com/library/ios/documentation/uikit/reference/UITableView_Class/Reference/Reference.html#//apple_ref/occ/instm/UITableView/reloadData
i have created a UItableView named _tableView, here is the code for table view in view.m
- (void)viewDidLoad
{
[super viewDidLoad];
tableData = [NSArray arrayWithObjects:#"Team Members",#"MembershipStatus",#"relation of status",#"Registerd events", nil];
[_tableView setUserInteractionEnabled:TRUE];
[_tableView reloadData];
}
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return [tableData count];
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
return 54;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
[newCell setUserInteractionEnabled:TRUE];
newCell.textLabel.text = [tableData objectAtIndex:indexPath.row];
return newCell;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
NSLog(#"id");
}
while executing the code I got the table correctly. But when i am selecting one of the list item it won't respond. (i.e., the NSLog is not working). did i miss something ?? or what i do wrong ??
In your -viewDidLoad write:
[_tableView setDelegate:self];
check if the tableview's delegate is set to your viewController
#interface MyView : UIViewController <UITableViewDelegate, UITableViewDataSource>
in viewDidLoad:
self.tableview.delegate=self;
Check with your code
#interface ClassName : UIViewController <UITableViewDelegate, UITableViewDataSource>
in viewDidLoad:
self.tableView.delegate = self;
self.tableView.dataSource = self;
or
_tableView.delegate = self;
_tableView.dataSource = self;
check in .h file
#interface MyView : UIViewController is set or Not. if its not set then Set 1st there.
then in .m File while Creating TableView there
you just write
_tableView.delegate = self;
_tableView.dataSource = self;
I want to create a new class, MYTableView which inherits from UITableView. MYTableView will have an NSArray property that stores all of its data. This means I no longer have to worry about implementing numberOfRows and cellForRow, because MYTableView will be its own datasource and provide the necessary data. Additionally, I want MYTableView to have some custom logic in the didSelectRow method, so I set it up to be its own delegate as well.
Now here is the problem... I have a TVC (a UITableViewController) object which has a MYTableView instance. I want to override the logic in the didSelectRow method. That is, I want the logic from MYTableView to happen in addition to TVC's logic.
Solution 1: tableView.delegate = TVC
Obviously this won't work. If I try to set the tableView's delegate back to the TVC, then yes, the TVC's didSelectRow method is called, but then the MYTableView's logic is never called, because we hijacked the delegate.
Solution 2: tableView.delegate2 = TVC
I could create a second delegate (see code below) and set TVC to be the delegate2. I would then call delegate2's didSelectRow method from within the tableView.delegate's didSelectRow method.
Problems:
Having delegate and delegate2 is ugly. Additionally, if the user sets delegate to be something else, things break.
I would have to forward every single event that TVC might want to implement.
Questions:
Since this is not an optimal solution. Can you think of any better solutions?
If I have no control over the inherited class (e.g. if I am inheriting from UITableView), what are my options?
If I do have control over the inherited class (e.g. I am using a hypothetical UITableView of which I own the source code to), what would be the best way to implement it to allow this sort of functionality?
Notes:
The didSelectRow currently only has 1 line of code in these examples. Obviously if it was only one line of code, then this wouldn't be a big problem, I would find a way around it. The real code is many lines of code.
Full code follows: (for solution 2)
// ---------------------------------------------------
// VC.m
// ---------------------------------------------------
#implementation VC
- (MYTableView *)myTableView {
return (MYTableView *)self.tableView;
}
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
self.navigationItem.leftBarButtonItem = self.editButtonItem;
UIBarButtonItem *addButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAdd target:self action:#selector(insertNewObject:)];
self.navigationItem.rightBarButtonItem = addButton;
self.myTableView.delegate2 = self;
}
- (void)insertNewObject:(id)sender
{
static NSInteger i = 0;
if (i % 3 == 0) {
[self.myTableView insertObject:[NSNull null] inDataArrayAtIndex:0];
} else {
[self.myTableView insertObject:[NSDate date] inDataArrayAtIndex:0];
}
i++;
}
#pragma mark - MYTableViewDelegate
- (void)myTableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
NSLog(#"Selected row: %#", indexPath);
}
#end
// ---------------------------------------------------
// MYTableView.h
// ---------------------------------------------------
#class MYTableView;
#protocol MYTableViewDelegate <UITableViewDelegate>
#optional
- (void)myTableView:(MYTableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath;
#end
#interface MYTableView : UITableView <UITableViewDataSource, UITableViewDelegate>
#property (nonatomic, weak) id<MYTableViewDelegate> delegate2;
- (void)insertObject:(NSObject *)object inDataArrayAtIndex:(NSUInteger)index;
#end
// ---------------------------------------------------
// MYTableView.m
// ---------------------------------------------------
#interface MYTableView ()
#property (nonatomic, strong) NSMutableArray *dataArray;
#end
#implementation MYTableView
- (void)commonInit {
self.dataArray = [NSMutableArray array];
self.dataSource = self;
self.delegate = self;
}
- (id)initWithCoder:(NSCoder *)aDecoder {
self = [super initWithCoder:aDecoder];
if (self) {
[self commonInit];
}
return self;
}
- (id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
[self commonInit];
}
return self;
}
- (void)insertObject:(NSObject *)object inDataArrayAtIndex:(NSUInteger)index {
[self.dataArray insertObject:object atIndex:index];
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:index inSection:0];
[self insertRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
}
#pragma mark - UITableViewDataSource
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return [self.dataArray count];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
NSString * const identifier = #"String Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:identifier];
if (!cell) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:identifier];
}
cell.textLabel.text = [self.dataArray[indexPath.row] description];
return cell;
}
#pragma mark - UITableViewDelegate
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
NSObject *object = self.dataArray[indexPath.row];
if ([NSNull null] == object) {
[self deselectRowAtIndexPath:indexPath animated:YES];
}
if ([self.delegate2 respondsToSelector:#selector(myTableView:didSelectRowAtIndexPath:)]) {
[self.delegate2 myTableView:self didSelectRowAtIndexPath:indexPath];
}
}
#end
You can use two delegates, but override the setDelegate method to make things at least appear cleaner. I've done something similar in a subclass of UITextView: https://github.com/mbachrach/GCPTextView
What if you made a separate class that just implements the UITableViewDataSource protocol based on your datasource. This can be an NSObject or I suppose your MyTableView if you really want to do it that way. It is a very highly reusable class which will make your life easier in the future.
As for the double delegate problem, what if you handled the selection of the row through UITableViewDelegate in MyTableView but setup a block-based system for selecting the view controller.
void(^RowSelected)(void)(NSObject *selectedObject, NSIndexPath *indexPath)
In Fixing UITextView on iOS 7, the author mentions that he automatically forwards all delegate methods in the source code for PSPDFTextView.
He accomplishes this by doing the following:
Set yourself up as the delegate by calling [super setDelegate:self].
Override setDelegate: to capture the external delegate in a property called realDelegate.
In any delegate methods you implement, forward them to the realDelegate (if it responds to that selector):
- (void)textViewDidBeginEditing:(UITextView *)textView {
id<UITextViewDelegate> delegate = self.realDelegate;
if ([delegate respondsToSelector:_cmd]) {
[delegate textViewDidBeginEditing:textView];
}
// custom code goes here
}
Implement respondsToSelector:, methodSignatureForSelector:, and forwardInvocation: to forward the rest of the methods:
- (BOOL)respondsToSelector:(SEL)aSelector {
return [super respondsToSelector:aSelector] || [self.realDelegate respondsToSelector:aSelector];
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
return [super methodSignatureForSelector:aSelector] ?: [(id)self.realDelegate methodSignatureForSelector:aSelector];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
id delegate = self.realDelegate;
if ([delegate respondsToSelector:anInvocation.selector]) {
[anInvocation invokeWithTarget:delegate];
}
}
One drawback to this method is that it will forward any call to realDelegate; not just those that are implemented in UITextFieldDelegate. For example, if your delegate is a UIViewController and you execute the following code:
self.textView.delegate = self;
[(id)self.textView viewDidLoad];
You would ideally want it to crash (since your UITextView subclass does not implement viewDidLoad). However, because any method called on the subclass will be forwarded to the delegate, the text view will see that it doesn't know how to handle viewDidLoad, so then it will ask the delegate; the delegate will report that it does know how to execute this code, and so it will call the delegate's viewDidLoad method!