I am very new to iOS, Living in a very small village with no help on iOS, I need help in my code where I can increase number of rows once I reach to row number 20.
I actually have more than 6000 lines, when try to show all of them in tableview, it takes lot of time, so I want to load all rows in efficient way, like increasing it like +20, +50 etc
here is my code:
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return a;
}
and I am trying to increase rows like this:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
if(indexPath.row == 20)
{
[_myTable reloadData];
[self.myTable beginUpdates];
a= a +50;
[self.myTable endUpdates];
}
if (indexPath.row > 60)
{
[self.myTable beginUpdates];
a= a +100;
[self.myTable endUpdates];
}
}
I declared myTable as property
I am sorry if my post is looking foolish I am at very basic position and sorry for my bad English.
The numbers of rows to be displayed in a tableView depends on data available i.e., the data to be displayed in the UITableView.
Let an array "Countries" containing list of 3 countries:
NSArray *countries;
countries = #[#"India",#"USA",#"Germany",nil];
so, now the UITableView has 3 rows, so use this:
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return countries.count;
}
so if the countries list increased later, then it will work.
No need to increase number of rows in table manually.
If the response is from server, then use this:
NSArray *dataArray = [responseObject objectForKey:#"countriesJson"];
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return dataArray.count;
}
Sample Project:
Please check my GitHub link below:
https://github.com/k-sathireddy/TableRowCountDynamic
Note: More images are loaded if we reach a certain limit.
You have to follow below algorithm or logic,
1)First just initialize your array with the 20 objects and that array will use for the display in a UITableView as a datasource.
2)Then you need to implement the UIScrollView delegate method, - (void)scrollViewDidScroll:(UIScrollView *)aScrollView in which you can write a some code like this:
- (Void) scrollViewDidScroll: (UIScrollView *) aScrollView
{
self.tbleSerachResult.frame=self.tbleSerachResult.frame;
if (aScrollView==self.tblOutlet) {
[currentTextfield resignFirstResponder];
CGPoint offset = aScrollView.contentOffset;
CGRect bounds = aScrollView.bounds;
CGSize size = aScrollView.contentSize;
UIEdgeInsets inset = aScrollView.contentInset;
float y = offset.y + bounds.size.height - inset.bottom;
float h = size.height;
float reload_distance = 10;
if(y > h + reload_distance)
{
//add more 20 records to your existing array which used for the UITableView data source and reload the UITableView.
//Put your load more data method here...
}
}
}
3) after this, you doesn't require any other thing to maintain this. Just implement the UITableView delegate and datasource methods as normally. It will work.
Try this way, if still facing any issue let me know I will give you example code.
Hope it works for you!! Happy coding :)
Related
I have a table view with custom cells (all configured in a subclass using auto layout).
The cells load fine, display fine, everything is fine.
The issue is when I am inserting more rows (at the bottom). The table view is representing a feed for posts, so when the user scrolls to the bottom, before reaching the last cell, I load new posts, and then insert them into the table.
When I do this, I get this weird glitchy effect where the cells randomly come down (behind the previous cells) into place, the table view scrolls up a bit, messy.
CODE AT BOTTOM
I've uploaded a clip of me scrolling. When you see the activity indicator,
I stop scrolling. The rest of the movement is from the glitchy behavior.
Is the reason for the glitch because the cells are being drawn with auto-layout?
I would hope not, but idk..I'm not sure what to do regarding a solution. If anyone has any ideas please let me know.
FYI:
I have this (of course, since the cells are all using auto layout)
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
return UITableViewAutomaticDimension;
}
I've tried setting the estimated height to an "average" of the expected cell heights, around 65. No difference.
Update
Here's some code:
HomeViewController.m --> viewDidLoad
...
self.tableView = [KATableView.alloc initWithFrame:CGRectZero style:UITableViewStylePlain];
self.tableView.delegate = self;
self.tableView.dataSource = self;
self.tableView.refreshDelegate = self;
self.tableView.estimatedRowHeight = 75;
self.tableView.rowHeight = UITableViewAutomaticDimension;
[self.view addSubview:self.tableView];
// Constrains to all 4 sides of self.view
[SSLayerEffects constrainView:self.tableView toAllSidesOfView:self.view];
my table view data source
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
if (!self.dataManager.didFinishFetchingData) return 4;
if (self.contentObjects.count == 0) return 1;
if (self.dataManager.moreToLoad) return self.contentObjects.count + 1;
return self.contentObjects.count + 1;
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
return UITableViewAutomaticDimension;
}
- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath {
return UITableViewAutomaticDimension;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
MYObject *object = self.contentObjects[indexPath.row];
SomeTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:object.documentID];
if (!cell) {
cell = [SomeTableViewCell.alloc initWithStyle:UITableViewCellStyleDefault reuseIdentifier:object.documentID];
cell.delegate = self;
} else [cell startListeningForChanges];
return cell;
}
Here is how I am loading more data and adding it to the table view..
- (void)getHomeFeedData:(nullable void(^)(BOOL finished))completed {
[self.dataManager fetchHomeFeedDataForFeedOption:self.homeNavController.feedFilterOption completion:^(NSError * _Nullable error, NSArray<__kindof KAObject *> * _Nullable feedObjects) {
if (error != nil) {
NSLog(#"something went wrong: %#", error.localizedDescription);
if (completed) completed(NO);
return;
}
NSInteger originalCount = self.contentObjects.count;
if (self.dataManager.isFirstTimeLoading) self.contentObjects = feedObjects.mutableCopy;
else {
if (self.dataManager.isGettingNew) for (MYObject *obj in feedObjects) [self.contentObjects insertObject:obj atIndex:0];
else if (feedObjects.count > 0) [self.contentObjects addObjectsFromArray:feedObjects];
}
if (feedObjects.count > 0) {
if (self.dataManager.isFirstTimeLoading) [self.tableView reloadData];
else {
[self.tableView insertCells:feedObjects forSection:0 startingIndex:self.dataManager.isGettingNew? 0 : originalCount];
}
} else if (self.dataManager.isFirstTimeLoading) [self.tableView reloadData];
if (completed) completed(YES);
}];
}
NOTE:
[self.tableView insertCells:feedObjects forSection:0 startingIndex:self.dataManager.isGettingNew? 0 : originalCount];
is simply this:
- (void)insertCells:(nullable NSArray *)cells forSection:(NSInteger)section startingIndex:(NSInteger)start {
if (!cells) return;
NSMutableArray *indexPaths = #[].mutableCopy;
for (id obj in cells) {
NSInteger index = [cells indexOfObject:obj] + start;
[indexPaths addObject:[NSIndexPath indexPathForRow:index inSection:section]];
}
[self insertRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationFade];
}
Update 2
My UITableViewCell subclass content is hidden ATM (too much difficulty in editing all my post content for the purpose of this post). I just have the subviews of each cell set to alpha = 0.f. It's just an image view, some labels, and some buttons.
No constraint issues in console, cells render perfectly when calling [self.tableView reloadData] so maybe there is something I'm doing wrong when inserting the cells?...
When you dealing with UITableView glitches:
Make sure you call UIKit API's on a main thread - turn on Main Thread checker
In your case, there might be an issue that fetchHomeFeedDataForFeedOption:completion: completion block is called not on a main thread.
Your insert is definitely wrong - all delete/insert/update/move calls for UITableView should be wrapped in beginUpdates/endUpdates
Your "load more" component at the bottom might be an issue. You need to address how it's managing contentSize/contentOffset/contentInset of table view. If it does anything but manipulating contentInset - it does wrong job.
While it's hard without debugging the whole solution, I bet options 2 & 3 are the key problems out there.
I have added some animation to a table view to make it look nicer when it loads. In the controller's viewDidLoad I make an asynchronous request for data and when it returns the table view is populated.
When my table loads the cells are revealed one by one. (I took inspiration from this excellent guide).
- (void)tableFadeInAnimation {
//[_venueTableView reloadData];
NSArray<UITableViewCell *> *cells = _venueTableView.visibleCells;
NSInteger index = 0;
for (UITableViewCell * m in cells){
UITableViewCell *cell = m;
cell.alpha = 0;
[UIView animateWithDuration:1 delay:0.25 * index options:0 animations:^(){
cell.alpha = 1;
} completion:nil];
NSLog(#"end of table animation");
index += 1;
}
}
My problem with running this as an initialising function is that once this finishes my table has no more animations to perform. I then took this principle to cellForRowAtIndexPath (removing the loop).
cell.alpha = 0;
[UIView animateWithDuration:2.0 animations:^(){
cell.alpha = 1;
}];
This would load all the cells together but would animate new cells appearing on the table.
cell.alpha = 0;
[UIView animateWithDuration:1 delay:0.05 * indexPath.row options:0 animations:^(){
cell.alpha = 1;
} completion:^(BOOL finished){
NSLog(#"animation complete");
}];
This made the table load each cell 1 by 1 however it is tied to all the cells (not the visible ones) so the further you go down the table, the longer the loading time for the cell.
Also when you move back up the table, all the older cells reanimate onto the table. I want the old cells to remain and the new cells to animate. Is there a way I can keep track of which cells have been loaded and only animate brand new, never before seen cells?
You should have a property to keep track index of last cell which is displayed (name lastCellDisplayedIndex). Only animate cells which have index less than lastCellDisplayedIndex. Each time call reloadData, reset lastCellDisplayedIndex = -1.
Try my below code.
#property (nonatomic, assign) NSUInteger lastCellDisplayed;
- (void)viewDidLoad {
[super viewDidLoad];
[self reloadTableView];
}
// Use this method each time you want to reload data of tableView
// instead of |reloadData| method
- (void)reloadTableView {
_lastCellDisplayedIndex = -1;
[self.tableView reloadData];
}
- (void)tableView:(UITableView *)tableView didEndDisplayingCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {
// Update |_lastCellDisplayedIndex| each time a cell is displayed
_lastCellDisplayedIndex = MAX(indexPath.row, _lastCellDisplayedIndex);
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
...
// Only animate cells which have |indexPath.row| < |_lastCellDisplayedIndex|
if (_lastCellDisplayedIndex < indexPath.row) {
cell.alpha = 0;
[UIView animateWithDuration:1 delay:0.05 * indexPath.row options:0 animations:^(){
cell.alpha = 1;
} completion:^(BOOL finished){
NSLog(#"animation complete");
}];
} else {
cell.alpha = 1;
}
...
}
The best approach for this is to add your animation block and any change to your cell's frame or alpha, in the tableView:willDisplayCell:forRowAtIndexPath: method of your UITableViewDelegate
My recommended approach, assuming you have some backing data source to provide data in cellForRowAtIndexPath, is to add some mutable property hasBeenDisplayed to your model objects (or an NSDictionary that maps each model object to a bool that indicates whether or not it has been displayed). This logic is complicated enough that you want some logic code to ensure the consistency of the view code. Then, once you have this property, you can call your custom animation in cellForRowAtIndexPath if the cell has not yet been displayed.
Thanks to #trungduc for the answer, I'm posting the completed solution to this in the hopes people will find it useful. To stop the table drawing cells that have already appeared you need to implement a variable to track the maximum index that has been displayed on the table, lastCellDisplayedIndex. In #trungduc's answer he put this variable in the - (void)tableView:(UITableView *)tableView didEndDisplayingCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath method however I found this created some errors making some cells redraw themselves. I had a read up on the difference between the cellForRow and cellWillDisplay methods and it seemed like the best place to put the animation was cellWillDisplay as the cell has been initialised and is apparently the place you should be performing UI tweaks to a cell (like animations!).
This method looks like this:
-(void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath{
lastCellDisplayedIndex = MAX(indexPath.row, lastCellDisplayedIndex);
NSLog(#"lastCellDisplayedIndex = %ld, indexPath for Cell = %ld", lastCellDisplayedIndex, indexPath.row);
if (lastCellDisplayedIndex <= indexPath.row){
cell.alpha = 0;
[UIView animateWithDuration:2.0 animations:^(){
cell.alpha = 1;
}];
if (lastCellDisplayedIndex == totalCellsToDisplay - 1){
NSLog(#"END OF TABLE ANIMATIONS!");
lastCellDisplayedIndex = totalCellsToDisplay + 1;
}
}
else {
cell.alpha = 1;
}
}
This method handles almost everything. It will first change the value of lastCellDisplayedIndex to the value of the max index the table has seen. Next it will decide whether the cell it is handling should be animated or left as is. I also had to add a guard variable (of sorts), totalCellsToDisplay would act as your tables datasource array: -
(NSInteger)tableView:(nonnull UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return totalCellsToDisplay;
}
So in your real app you would instead have
- (NSInteger)tableView:(nonnull UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return yourTableDataArray.count;
}
The reason I am checking the maximum number of cells being drawn is if you just have this code:
lastCellDisplayedIndex = MAX(indexPath.row, lastCellDisplayedIndex);
if (lastCellDisplayedIndex <= indexPath.row){}
then the maximum index will never go higher than the final cell, so this cell will be reanimated every-time you scroll up and down. To fix this when the indexPath = the total cells - 1 (because of zero index) then you bump the value of lastCellDisplayedIndex up so that no more cells will ever get drawn.
Finally we need to solve the issue of how many cells the table will initially draw. I'm not sure quite how this works but in my testing it would always draw 15 cells (if I returned more than 15). Anyway I implemented both my staggered load animation and fixed this problem with my loading animation function.
- (void)tableFadeInAnimation {
[_myTable reloadData];
NSArray<UITableViewCell *> *cells = _myTable.visibleCells;
NSInteger index = 0;
for (UITableViewCell * m in cells){
UITableViewCell *cell = m;
cell.alpha = 0;
[UIView animateWithDuration:0.5 delay:0.25 * index options:0 animations:^(){
cell.alpha = 1;
} completion:^(BOOL finished){
lastCellDisplayedIndex = _myTable.visibleCells.count;
NSLog(#"Table Animation Finished, lastCellDisplayed Index = %ld", lastCellDisplayedIndex);
}];
NSLog(#"end of table animation");
index += 1;
}
}
I used the completion block of the function to set the value of lastCellDisplayed equal to the number of cells that are visible. Now the table view will animate all new cells.
Hope this helps and thanks to #trungduc for the answer!
I have an interesting scenario. I have some custom UITableViewCells designed in separate xib files. One of them have an UIImageView, which will load images whom size are not constant, so it means UIImageView's height must be flexible. The cell also has a UIView containing some UILabels, and UIView's size is constant, say 100. I want to expand and collapse the cells on didselectrowatindexpath event. To collapse the cell I have to hide the UIView that have some labels. Please guide me in this regard to achieve the goal.
Also my question is "How can I calculate the height of the row when cell is expanded and collapsed." Thanks
Edit: This is what I have tried. . .But failed
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
static DynamicTableViewCell *cell = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
cell = [self.tableView dequeueReusableCellWithIdentifier:CellIdentifier];
});
[self setUpCell:cell atIndexPath:indexPath];
CGFloat cellHeight = [self calculateHeightForConfiguredSizingCell:cell];
if(cellHeight >0) {
if(cell.isExpanded) {
return cellHeight;
}
else
{
return (cellHeight - detailViewHeight); // cell.detailView.frame.size.height = 100
}
}
else {
return cellHeight;
}
}
First of all you should not have any reference to instance to your cell, because of performance reasons.
Second you should use models to build your cells in the right way. Provided code does not show usage of model storage at all.
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
// Say you have a array of models NSArray<YourModel *> *dataSource;
YourModel *model = dataSource[indexPath.row]; // hope you have only one section.
}
Third it's a good way to use any architecture like MVVM or VIPER, or MVC, because if you have no one you will probably have problems in future support of your product. So in case of MVVM YourModel is like ViewModel.
To define a state of dynamic height cell, you use property isExpanded. That's a good point, but it should be defined in another place - YourModel. If you'll do that in a proper way, you'll know the state of cell without the cell, actually.
#interface YourModel ...
#property BOOL isExpanded;
#end
Be sure that you correctly change your state in didSelect:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
YourModel *model = dataSource[indexPath.row];
model.isExpanded = !model.isExpanded; // inverse works like switch
// Then relayout your row
[tableView beginUpdates];
// I commented next line because it might not be needed for you
// Test it please
// [tableView reloadRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationNone];
[tableView endUpdates];
}
So back to heightForRow:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
// Say you have a array of models NSArray<YourModel *> *dataSource;
YourModel *model = dataSource[indexPath.row]; // hope you have only one section.
// It's better to extract next code to function or any class, but not in your YourModel.
CGSize imageSize = [model.image doSizeCalulation]; // Use your algorithm to calculate image size
CGSize shortText = [model.shortText sizeWithAttributes:#[/* font attributes */]];
// If the cell is expanded then we should calculate height of text else height is zero.
CGSize fullText = model.isExpanded ? [model.fullText sizeWithAttributes:#[/* font attributes */]]; : 0;
// just sum
return imageSize.height + shortText.height + fullText.height;
}
Another way to achieve this to use UITableViewAutomaticDimension (just return it in heightForRow method). In that case you should properly setup constraints, and in runtime change the constant of fullText's height constraint depending on isExpanded property.
I Have a one table view and i have two array. My arrays name AllItems and SpecialItems. I Use segment control. I wantto if segment value is 0 tableview load AllItems Array, When change segment value and value is = 1 than mytableview reload tada but SpecialItems array. Can u help me please. Thanks.
I solved this problem with table tag.
- (IBAction)segmentControlChanged:(UISegmentedControl *)sender {
if (sender.selectedSegmentIndex == 1) {
mytable.tag = 1;
}
else
{
mytable.tag = 0;
}
[mytable reloadData];
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
if(tableView.tag==1)
{
return [specialItems count];
}
else
return [allItems count];
}
You could create two data source classes that implement all the UITableViewDataSource methods: one for AllItems and one for SpecialItems. To switch between the two, connect a valueChanged action. In the method that is called, set the data source and reload the table view.
- (void)valueChange:(UISegmentedControl *)sender
{
if (/* condition for all items */) {
self.tableView.dataSource = self.allItemsDataSource;
} else {
self.tableView.dataSource = self.specialItemsDataSource;
}
[self.tableView reloadData];
}
I would personally create an array which the data is loaded from. Put this in your implementation:
NSArray * _tableData
Then in your viewDidLoad just allocate this for the array which we want it to start on.
_tableData = [[NSArray alloc] initWithArray:allItems];
This initially loads the data we will always see as the segment control starts on index 0. We have to set the initial data somewhere so the tableView loads with some data in it.
Then set the number of rows and the cellForRowAtIndex to pick up from the _tableData array
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return _tableData.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView_ cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell * cell = [tableView_ dequeueReusableCellWithIdentifier:bCell];
// Here we use the specific array as we would normally
return cell;
}
This step means the tableView will load with the array. Even if the array is empty the view will still load as the number of cells will be zero.
Now in our value changed function we can reset the array as we need to:
- (IBAction)segmentControlChanged:(UISegmentedControl *)sender {
if (sender.selectedSegmentIndex == 1) {
_tableData = allItems;
}
else {
_tableData = specialItems;
}
[self.tableView reloadData];
}
You just need to make sure the segment control changed is linked up in the XIB file (or programatically) and that you reload the table after choosing the array.
This kind of thing is actually really easy to do. I would definitely recommend working it through step by step if you're having trouble. Make sure each step is working before applying the next:
Get the tableView loading with both sets of data individually
Confirm that the segment control is calling the change function when clicked
Then that should do it
I am new to ios programming so bear with me if the question is simple. I have a core data table mapped to a table view controller. The data in it currently looks as follows - there is one prototype cell:
I need to sum up the data by dates and show the details of each date in a different section with the summed up total coming up as the first row. Something like:
My question is is this doable? I am thinking I need to create sections and two prototype cells within each table cell. Would appreciate quick feedback.
Thanks all!
The easy way to do this is using section headers. You can either use a single string (#"%#: %#", date, total) or a wrapper view with a label on the left for the date and on the right for the total.
-(NSString *) tableView:(UITableView *)tv titleForHeaderInSection:(NSInteger)s
{
NSString *dateString = [self dateStringForSection:s];
float total = [self totalForSection:s];
return [NSString stringWithFormat:#"%#: %0.2f", dateString, total];
}
Or
-(UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section
{
return [self wrappedHeaderForSection:s];
}
You'll have to implement dateStringForSection: or wrappedHeaderForSection: appropriately, of course.
The easiest way is to style your UITableView to 'UITableViewStyleGrouped'.
UITableView *tab = [[UITableView alloc] initWithFrame:rect style:UITableViewStyleGrouped];
Or you can go to interface builder and in Table View change style from plain to grouped.
The style 'Grouped' divides your table into multiple sections.
The using UITableViewDelegate methods specify all the parameters.
// Tell the number of section in table
-(NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return numberOfSections;
}
//Tell the number of rows in each section
-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
if (section == 0)
{
return 2;
} else if(section == 1)...
}
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
if (indexPath.section == 0 && indexPath.row == 0)
{
//Show Amount for Jul 02, 2013
cell.textLabel.text = #"Jul 02, 2013";
cell.detailTextLabel = #"20.35";
}
// Do the same for all rows and section in table.
}
For further reference -
http://mobisoftinfotech.com/iphone-uitableview-tutorial-grouped-table/
You should also definitely check out the Sensible TableView framework. Saves me tons of time when working with Core Data.