iOS Chips in UITableViewCell with dynamic height - ios

I have tried tirelessly to create a "chips" layout within a table view cell where the height of the cell expands to fill the contents but with no luck.
My research has landed me with this library https://github.com/zoonooz/ZFTokenField but it doesn't quite fill my needs. An example is like the Messages app where the "to" field expands to fill all the contacts, or primarily this image below is what I'm ultimately attempting to achieve:
Where the chips wrap to the next line where appropriate expanding the cell as it goes. The best I have so far is "sort of" working minus the cell height and the label at the start (label should be an easy fix so not the main concern)
I've tried many approaches but with no cigar and any help would be greatly appreciated. (each "chip" needs to be a button so I can tap to remove them). I've tried with a collection view in the table view cell (which I didn't like) and manually laying out each button (show below)
- (CGSize)updateChipsLayout {
self.minWidth = 0.0f;
self.minHeight = 0.0f;
if (!self.visibleButtons) {
self.visibleButtons = [NSMutableArray new];
}
for (UIButton *button in self.visibleButtons) {
[button removeFromSuperview];
}
[self.visibleButtons removeAllObjects];
CGRect oldButtonRect = CGRectZero;
oldButtonRect.origin.x = self.buttonPadding;
oldButtonRect.origin.y = self.buttonPadding;
CGFloat maxHeight = 0.0f;
for (ChipsData *data in self.chips) {
NSString *autoLayout_orientation = #"H";
NSLayoutFormatOptions autoLayout_options = NSLayoutFormatAlignAllLeft;
UIButton *button = [data generateButton];
CGSize buttonSize = button.bounds.size;
CGFloat newX = oldButtonRect.origin.x + oldButtonRect.size.width + self.buttonPadding;
if (newX + buttonSize.width > self.bounds.size.width) {
newX = self.buttonPadding;
oldButtonRect.origin.x = newX;
oldButtonRect.origin.y += maxHeight + self.buttonPadding;
maxHeight = 0.0f;
autoLayout_orientation = #"V";
autoLayout_options = NSLayoutFormatAlignAllTop;
}
if (buttonSize.height > maxHeight) {
maxHeight = buttonSize.height;
}
self.minHeight = MAX(self.minHeight, buttonSize.height);
self.minWidth = MAX(self.minWidth, buttonSize.width);
button.frame = CGRectMake(
newX,
oldButtonRect.origin.y,
button.frame.size.width,
button.frame.size.height
);
button.tag = self.visibleButtons.count;
[button addTarget:self action:#selector(buttonTouchUp:) forControlEvents:UIControlEventTouchUpInside];
[self addSubview:button];
oldButtonRect = button.frame;
[self.visibleButtons addObject:button];
}
self.cellSize = CGSizeMake(
oldButtonRect.origin.x + oldButtonRect.size.width + self.buttonPadding,
oldButtonRect.origin.y + maxHeight + self.buttonPadding
);
//[self setHeight:self.cellSize.height];
[self invalidateIntrinsicContentSize];
return self.frame.size;
}
The button generation logic isn't that complex either:
- (UIButton *)generateButton {
UIColor *backgroundColor = [UIColor colorWithWhite:0.1f alpha:1.0f];
UIColor *backgroundColorHighlighted = [UIColor colorWithWhite:0.4f alpha:1.0f];
UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
NSMutableAttributedString *attrText = [[NSMutableAttributedString alloc] initWithAttributedString:self.attributedText];
[attrText addAttributes:#{
NSForegroundColorAttributeName: [UIColor whiteColor],
NSBackgroundColorAttributeName: backgroundColor,
}
range:NSMakeRange(0, attrText.length)
];
[button setAttributedTitle:attrText forState:UIControlStateNormal];
[button setBackgroundImage:[self generateBackgroundWithColor:backgroundColor] forState:UIControlStateNormal];
[button setBackgroundImage:[self generateBackgroundWithColor:backgroundColorHighlighted] forState:UIControlStateHighlighted];
[button sizeToFit];
[button.layer setCornerRadius:8.0f];
[button.layer setMasksToBounds:YES];
[button setClipsToBounds:YES];
return button;
}
I assume I'll need a function to determine the cell height outside of the cell as I can't get a reference to the cell to determine it's height until I tell it what it's height should be.
PS yes I am aware that I am still using Objective-C when Swift is available.

The size of the cell is not determined by the cell, but by the tableView's delegate, specifically
tableView:heightForRowAtIndexPath:
So, you'll have to get the size of how high each cell will be and return that height. Also, if the data for each item in the cell does not change while the user is in that view, I would recommend storing the height for each individual index in an array once it is known so you can avoid expensive calculations.
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
// We are just going to assume a single section for brevity
// And we also have an array of CGFloats that is the same size as the # of rows
CGFloat height = knownHeights[indexPath.row];
if (height == 0) // our default value when we created the array
{
// You'll have to write the get custom height function
height = [self getHeightForItemAtIndexPath:indexPath];
knownHeights[indexPath.row] = height;
}
return height;
}

Related

Laggy when switching tabBarController to a tableView with many cells

My app is based on a tabBarController and has 2 tabs.
I load data for the 2nd tab in the background while the app launches at tab 1, so users don't need to wait when they click tab 2.
My problem is: it's laggy to switch to tab 2 if I load the data in advance and there is much data to show in cells of the tableView belonging to tab 2. If there are not many cells to show, then it's not laggy at all.
I guess it's because generating cells is time-consuming, so the view is blocked when there are too many cells. How do I optimize this?
Important! This code has not been tested in the XCode, but contains some parts from real projects.
The CellView class could be created either as a Nib or even manually using calculateCellHeight method from ServicesHelper.m. In both cases the layoutSubviews method must be implemented where the resize detailTextLabel UILabel code to be placed. The CellView labels Font and Color must be the same as used in calculateCellHeight method.
ServicesHelper.m
#define FontRegular(fontSize) [UIFont fontWithName:#"HelveticaNeue" size:fontSize]
// the height of fixed part of the cell, fixed height UILabel + some padding
#define kFixedPartCellHeight 20
#define kLeftPaddingWidth 20
#define kLandscapeHeightKey #"landscapeKey"
#define kPortraitHeight #"portraitKey"
+ (NSDictionary *) calculateCellHeight: (NSString *) text {
// dynamic height label
UILabel *detailTextLabel = [UILabel new];
UIFont *mainFont = FontRegular(14.0);
// specifying font and colour to be used inside cell is important to get precise frame rect
[detailTextLabel setFont: mainFont];
[detailTextLabel setTextColor:[UIColor blackColor]];
detailTextLabel.lineBreakMode = NSLineBreakByWordWrapping;
detailTextLabel.numberOfLines = 0;
[textLabel setBackgroundColor: [UIColor clearColor]];
[detailTextLabel setBackgroundColor: [UIColor clearColor]];
// get the width of the cell for both orientations
CGRect screenRect = [[UIScreen mainScreen] bounds];
CGFloat landscapeWidth = MAX (screenRect.size.width, screenRect.size.height);
CGFloat portraitWidth = MIN (screenRect.size.width, screenRect.size.height);
// kLeftPaddingWidth - is just a white space left and right to the UILabel inside cell
// we set the UILabel width with maximum possible height, then set text and shrink it using sizeToFit to get the exact size
detailTextLabel.frame = CGRectMake (0, 0 , landscapeWidth - kLeftPaddingWidth * 2, CGFLOAT_MAX);
textLabel.frame = detailTextLabel.frame;
detailTextLabel.text = text;
[detailTextLabel sizeToFit];
CGFloat landscapeHeight = detailTextLabel.frame.size.height + kFixedPartCellHeight;
detailTextLabel.frame = CGRectMake (0, 0 , portraitWidth - kLeftPaddingWidth * 2, CGFLOAT_MAX);
textLabel.frame = detailTextLabel.frame;
detailTextLabel.text = text;
[detailTextLabel sizeToFit];
CGFloat portraitHeight = detailTextLabel.frame.size.height + kFixedPartCellHeight;
return #{kLandscapeHeightKey: landscapeHeight, kPortraitHeightKey: portraitHeight};
}
TableView.h
#property (nonatomic, strong) NSMutableArray *arrayOfTexts;
#property (nonatomic, strong) NSMutableDictionary *dictOfHeights;
TableView.m
- (void) precalculateHeight {
if (nil == self.dictOfHeights) self.dictOfHeights = [NSMutableDictionary new];
CGRect screenRect = [[UIScreen mainScreen] bounds];
// height of two screens
CGFloat maxHeight = MAX (screenRect.size.width, screenRect.size.height) * 2;
CGFloat totalHeight = 0;
CGFloat portraitHeight;
CGFloat landscapeHeight;
NSDictionary *dictHeights;
for (int i = 0; i < self.arrayOfTexts.count; i++ ) {
dictHeights = [ServicesHelper calculateCellHeight: arrayOfTexts[i]];
portraitHeight = [dictHeights[kPortraitHeightKey] floatValue];
[self.dictOfHeights setValue: dictHeights forKey:#(i)];
totalHeight += portraitHeight;
if (totalHeight > maxHeight) break;
}
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
if (self.dictOfHeights && self.dictOfHeights[#(indexPath.row)]) {
return UIDeviceOrientationIsPortrait([UIDevice currentDevice].orientation) ?
[self.dictOfHeights[#(indexPath.row)][kPortraitHeightKey] floatValue] :
[self.dictOfHeights[#(indexPath.row)][kLandscapeHeightKey] floatValue];
} else {
// you decide, call calculateCellHeight for particular row, right from here, or also calculate rows height for the coming set of cells
// #todo:
}
return CGFLOAT_MIN;
}

Get dynamic Coordinates of created subview ios

I created a method that creates a subview based on the input size so there is not a defined and default corner CGRect values.I want to create a dismiss button that will be located at the top left corner of each subview. Currently I'm using this method to add it, but the problem with this is that the values for CGRectMake can not be static.
CGRect tapToDismissFrame = CGRectMake(50.0f, 50.0f, 400.0f, 400.0f);
UIButton *tapToDismiss = [[UIButton alloc] initWithFrame:tapToDismissFrame];
tapToDismiss.backgroundColor = [UIColor clearColor];
tapToDismiss.titleLabel.font = [UIFont fontWithName:[NSString stringWithFormat:#"Helvetica-Bold"] size:20];
[tapToDismiss setTitle:#"⊗" forState:UIControlStateNormal];
[tapToDismiss setTitleColor:[UIColor colorWithRed:173.0/255.0 green:36.0/255.0 blue:36.0/255.0 alpha:1.0] forState:UIControlStateNormal];
[tapToDismiss addTarget:self action:#selector(tapToDismissClicked:) forControlEvents:UIControlEventTouchUpInside];
[self.maskView addSubview:tapToDismiss];
How can i get the dynamic values in order to create it in the top left or right corner.
You should use CGGeometry functions to calculate correct frame for your dismiss button. Suppose you created that button. After that point here is a sample code:
[button sizeToFit];
CGRect maskViewBounds = self.maskView.bounds;
CGFloat top = CGRectGetMinY(maskViewBounds);
CGRect left = CGRectGetMaxX(maskViewBounds);
CGFloat buttonHeight = button.size.height;
CGFloat buttonWidth = button.size.width;
CGFloat topMargin = 5.0f;
CGFloat leftMargin = 5.0f;
CGPoint buttonCenter = CGPointMake(top + buttonHeight / 2.0 + topMargin, left - buttonWidth / 2.0 - leftMargin);
button.center = buttonCenter;
Note: I didn't write this code in Xcode. So there may be type errors.

How to make a UIButton grid according to size of superview in iOS?

I am trying to make a UIButton Grid with respect to size of superView.Most of the solutions that I have seen are such where buttons are added to scrollView .But what if I don't want scrollable grid and just a normal grid .How can I resize my buttons depending on the size of its superview where they are plotted?.Has anyone tried it?Any help is really appreciated.
-(void)createLayout{
int width =30;
int height =30;
int leftMargin =10;
int topMargin =10;
int xTile;
int yTile;
int xSpacing = 50;
int ySpacing = 50;
for (int c=1; c<=5; c++) {
for (int r=1; r<=10; r++) {
NSLog(#"row : %d col : %d",r,c);
yTile = ((c*ySpacing)+topMargin);
xTile = ((r*xSpacing)+leftMargin);
Test *btn = [Test buttonWithType:UIButtonTypeCustom];
btn.row = r;
btn.column =c;
[btn setTitle:[NSString stringWithFormat:#"%d,%d",btn.row,btn.column] forState:UIControlStateNormal ];
[btn.titleLabel setFont:[UIFont fontWithName:#"Arial" size:14]];
[btn setBackgroundColor:[UIColor redColor]];
[btn setFrame:CGRectMake(xTile, yTile, width, height)];
[self.scrollView addSubview:btn];
xTile=xTile+10;
}
yTile = yTile+10;
}
}
Well to answer your question I wrote a code. Something like this will help you. I have created this project for you here. Check that. Also it's not completely fine I would like you to work on it and made a commit when you have made it perfect.
- (UIView *) view : (CGSize) viewSize withButtons : (int) numberOfButtons{
CGRect frame = CGRectZero;
frame.size = viewSize;
UIView *view = [[UIView alloc]initWithFrame:frame];//considering View size is after margine
CGSize buttonSize = [self getButton:numberOfButtons sizeFor:viewSize];
for (int i = 1; i <= numberOfButtons; i++) {
UIButton *button = [[UIButton alloc] initWithFrame:[self getButton:buttonSize frameForIndex:i inView:viewSize]];
[button setTitle:[[NSNumber numberWithInt:i] stringValue] forState:UIControlStateNormal];
[button setTitleColor:[UIColor darkGrayColor] forState:UIControlStateNormal];
[button.layer setBorderColor:[UIColor lightGrayColor].CGColor];
[button.layer setBorderWidth:1.0f];
[view addSubview:button];
}
return view;
}
- (CGRect) getButton : (CGSize) buttonSize frameForIndex : (int) buttonIndex inView : (CGSize) containerSize{
int x = buttonIndex % (int)(containerSize.width / buttonSize.width);
x *= buttonSize.width;
int y = buttonIndex % (int)(containerSize.height / buttonSize.height);
y *= buttonSize.height;
CGRect frame = CGRectZero;
frame.origin = CGPointMake(x, y);
frame.size = buttonSize;
return frame;
}
- (CGSize) getButton : (int) numberOfButtons sizeFor : (CGSize) containerSize{
double containerArea = containerSize.width * containerSize.height;
double areaOfOneButton = containerArea / numberOfButtons;
double edgeOfOneButton = sqrt(areaOfOneButton);
edgeOfOneButton = (edgeOfOneButton > containerSize.width) ? containerSize.width : edgeOfOneButton;
return CGSizeMake(edgeOfOneButton, edgeOfOneButton);
}

Add buttons to UIView

I would like to add buttons to an UIView. The number of buttons varies from view to view.
I want to achieve the following:
I tried to add the Buttons (in a foreach construct) like this:
UIButton *but=[UIButton buttonWithType:UIButtonTypeRoundedRect];
but.frame= CGRectMake(200, 15, 15, 15);
[but setTitle:#"Ok" forState:UIControlStateNormal];
[but addTarget:self action:#selector(buttonAction) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:but];
But I don't know how to calculate the CGRectMake when it comes to an new line. How can I check the end of the line (UIView)? Or is it kinda stupid to add Buttons like this?
Thanks
Florian
You need to add calculate buttons size after you set the title. Then keep track of the current X and Y of the previous buttons and figure out if the newly created button can fit on the current "line". Otherwise go to the next one.
I suggest you to look at existing components and see how they do it, below are a couple links.
https://github.com/domness/DWTagList
https://github.com/andreamazz/AMTagListView
With the following code , you can place multiple buttons in one row as long as their total width and the gap is smaller then the view's width.
UIView *view = nil;
NSArray *allBtns = nil;
float viewWidth;
float currentY = 0;
float currentX = 0;
float btnGapWidth = 10.0;
float btnGapHeight = 2.0;
for (UIButton *btn in allBtns) {
CGRect rc = btn.frame;
BOOL shouldAddToNewLine = viewWidth - currentX - btnGapWidth < rc.size.width ? YES : NO;
if (shouldAddToNewLine) {
rc.origin = CGPointMake(0, currentY + rc.size.height + btnGapHeight);
currentX = 0;
currentY += rc.size.height + btnGapHeight;
} else {
//another row
rc.origin = CGPointMake(currentX + rc.size.width + btnGapWidth, currentY);
currentX += rc.size.width + btnGapWidth;
}
btn.frame = rc;
[view addSubview:btn];
}

Fill view with buttons at random positions

i want to fill area (UIView) with buttons(UIButton) so that they do not intersect with each others.
My idea:
create initial button at random position in view;
fill view with other buttons from initial button (count < 20) that they do not intersect ~10 pixels from each other.
What have i done so far:
I created method:
-(void)generateButtonsForView:(UIView *)view buttonCount:(int)count
{
//get size of main view
float viewWidth = view.frame.size.width;
float viewHeight = view.frame.size.height;
//set button at random position
UIButton *initialButton = [[UIButton alloc] initWithFrame:CGRectMake(arc4random() % (int)viewWidth,
arc4random() % (int)viewHeight,
buttonWidth, buttonHeight)];
[initialButton setBackgroundImage:[UIImage imageNamed:#"button"] forState:UIControlStateNormal];
[view addSubview:initialButton];
// set count to 20 - max number of buttons on screen
if (count > 20)
count = 20;
//fill view with buttons from initial button +- 10 pixels
for (int i=0;i<=count;i++)
{
//button
UIButton *otherButtons = [[UIButton alloc] init];
[otherButtons setBackgroundImage:[UIImage imageNamed:#"button"] forState:UIControlStateNormal];
...//have no idea what to do here
}
}
So i'm confused on the place where i want to generate the position of the other buttons, depending on the initial button. I do not know how to generate theirs position that they were at a distance of 5-10 pixels from each other... Any ideas how to realise this? Thanks!
Here's an example with views rather than buttons, but the concept is the same. I use CGRectInset to give the new potential view a buffer of 10 points around it, then look for whether the new view intersects any of the other views. If there's no intersection, add the subview, if there is, try again with a new random location.
-(void)generateButtonsForView {
float viewWidth = self.view.frame.size.width;
float viewHeight = self.view.frame.size.height;
UIView *initialView = [[UIView alloc] initWithFrame:CGRectMake(arc4random() % (int)viewWidth, arc4random() % (int)viewHeight, 50, 30)];
initialView.backgroundColor = [UIColor redColor];
[self.view addSubview:initialView];
int numViews = 0;
while (numViews < 19) {
BOOL goodView = YES;
UIView *candidateView = [[UIView alloc] initWithFrame:CGRectMake(arc4random() % (int)viewWidth, arc4random() % (int)viewHeight, 50, 30)];
candidateView.backgroundColor = [UIColor redColor];
for (UIView *placedView in self.view.subviews) {
if (CGRectIntersectsRect(CGRectInset(candidateView.frame, -10, -10), placedView.frame)) {
goodView = NO;
break;
}
}
if (goodView) {
[self.view addSubview:candidateView];
numViews += 1;
}
}
}

Resources