Lately I'm pulling my hair out because of this. I'm still a rookie iOS Developer therefore a simple solution would be appreciated.
I have a UIPageViewController with three views, the middle view is a UITableView which i populate with data programmatically and try to make a layout like a grid. When the UITableView has too many data the information gets outside of the screen.
I've tried to disable auto layout and add the UITableView to a UIScrollView, enabling the UITableView to scroll horizontal. I've managed to do that but somehow the interaction is messy and most of the times when trying to horizontal scroll the table it catches the UIPageViewController interaction. Also the vertical scroll of the table does not respond well too.
Is there a known solution for this situation?
After testing a few things I ended up with a acceptable solution using autolayout.
So first I added a UIScrollView and added the UITableView inside the UIScrollView.
I've enabled the Paging and disabled the Bounce Horizontally in the UIScrollView.
In the UITableView I've basically disabled everything relative to bouncing paging and scrolling.
Since the data is being populated programmatically to be some sort of a grid I have to know the total width of the longest row for that I've created a few methods to calculate the maximum width and arrange the labels.
I also created constraints for the table Width and Height which I calculate and update after all the calculations are done.
-(void) ArrangeTable
{
for (int i= 0; i < [arrayTable count]; i++) {
for (int j=0; j< [[arrayTable objectAtIndex:i ] count]; j++) {
UILabel * nameLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, 8.0, 95.0, 30.0)];
[nameLabel setText:[NSString stringWithFormat:#"%#",[[arrayTable objectAtIndex:i] objectAtIndex:j]]];
[nameLabel sizeToFit];
float widthIs = [nameLabel.text
boundingRectWithSize:nameLabel.frame.size
options:NSStringDrawingUsesLineFragmentOrigin
attributes:#{ NSFontAttributeName:nameLabel.font }
context:nil].size.width;
[self alignLabels:j currentWidth:[NSNumber numberWithFloat: widthIs]];
}
}
}
-(void) alignLabels:(int)colNumber currentWidth:(NSNumber*)labelWidth
{
if(colNumber >= [arrayLabelWidth count])
{
//Insert without position
[arrayLabelWidth addObject:labelWidth];
}else if ([labelWidth floatValue] > [[arrayLabelWidth objectAtIndex:colNumber] floatValue] )
{
[arrayLabelWidth replaceObjectAtIndex:colNumber withObject:labelWidth];
}
}
-(void) setMaxRowWidth
{
float widthTV =[[arrayLabelWidth valueForKeyPath:#"#sum.self"] floatValue] + ([arrayLabelWidth count]) * 10;
CGRect screenRect = [[UIScreen mainScreen] bounds];
float heightTV = [self tableViewHeight];
if(widthTV > screenRect.size.width)
{
tvWidth.constant = widthTV;
//svWidth.constant = valueTV;
}else{
if (UIDeviceOrientationIsPortrait(self.interfaceOrientation)){
//DO Portrait
tvWidth.constant = screenRect.size.width;
}else{
//DO Landscape
float calc = screenRect.size.width - self.view.frame.size.width;
tvWidth.constant = screenRect.size.width - calc;
}
}
tvHeight.constant = heightTV;
}
- (CGFloat)tableViewHeight
{
[tablePod layoutIfNeeded];
return [tablePod contentSize].height;
}
Related
I have five rows of cells each row containing four cells - 5x4. I am trying to accomplish almost an Apple watch like effect. The row in the center of the screen should have cell sizes of 100x100 and the rest return sizes of 80x80. When scrolled, the row moving away from the center should turn to 80x80 and the row moving into the center should turn 100x100.
I have implemented prepareLayout, layoutAttributesForElementsInRect, and layoutAttributesForItemAtIndexPath
So right now I have a center row of 100x100 cells and the rest 80x80.
To make it dynamic I implement shouldInvalidateLayoutForBoundsChange but nothing happens.
- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds{
NSMutableArray *allAttributes = [NSMutableArray arrayWithCapacity:self.layoutInfo.count];
[self.layoutInfo enumerateKeysAndObjectsUsingBlock:^(NSString *elementIdentifier,
NSDictionary *elementsInfo,
BOOL *stop) {
[elementsInfo enumerateKeysAndObjectsUsingBlock:^(NSIndexPath *indexPath,
UICollectionViewLayoutAttributes *attributes,
BOOL *innerStop) {
// NSLog(#"%f, %f", newBounds.origin.x, newBounds.origin.y);
if ((newBounds.origin.y < [[UIScreen mainScreen]bounds].size.height/2-50) && (newBounds.origin.y > [[UIScreen mainScreen]bounds].size.height/2+50)) {
CGRect frame = attributes.frame;
frame.size.width = 100.0f;
frame.size.height = 100.0f;
attributes.frame = frame;
}else{
CGRect frame = attributes.frame;
frame.size.width = 80.0f;
frame.size.height = 80.0f;
attributes.frame = frame;
}
[allAttributes addObject:attributes];
}];
}];
return YES;
}
shouldInvalidateLayoutForBoundsChange: is called pretty frequently (whenever the view is resized, or scrolled...), so you probably don't need to be doing all that work there. If you know your cells will always have a fixed size and position then you can just return NO here.
As long as you have implemented prepareLayout, layoutAttributesForElementsInRect, and layoutAttributesForItemAtIndexPath, then make sure you also implement collectionViewContentSize to return the total size of the content.
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;
}
I've got a UITableView and I want to add a button just underneath the table. The table is bigger than the screen so to get to the button, I need to scroll down to it. The problem is, the screen will scroll to just above where the button is. So I can drag the screen slightly and the button is visible but when I let go, the screen springs back and I can't get to the button.
The button is set up as follows
for(int i = 0; i < [buttonArray count]; i++)
{
UIButton *but = [buttonArray objectAtIndex:i];
float xPosition = but.frame.origin.x;
float xGap = (self.view.frame.size.width - (but.frame.size.width)*2)/3;
if ((i % 2 == 0) && (i < [buttonArray count] - 1))
{
xPosition = xGap;
}
else if ((i % 2 == 1) && (i < [buttonArray count] - 1))
{
xPosition = (xGap * 2) + but.frame.size.width;
}
else
{
xPosition = (self.view.frame.size.width - but.frame.size.width)/2;
}
//Add space between row of buttons
if(i > 0)
{
if(i % 2 == 0)
{
yPosition+=but.frame.size.height;
CGRect screenRect = [[tableView superview] bounds];
//Add extra space between row of buttons
float fGap = (screenRect.size.height / 100.0) * 4.5;
yPosition+=fGap;
}
}
CGRect rect = CGRectMake(xPosition, yPosition, but.frame.size.width, but.frame.size.height);
but.frame = rect;
[but addTarget:self action:#selector(genericMethod:) forControlEvents:UIControlEventTouchUpInside];
CGRect screenRect = [self.view bounds];
CGFloat fHeight = screenRect.size.height;
float fMargin = (fHeight / 100.0) * 0.8;
but.titleEdgeInsets = UIEdgeInsetsMake(0, 0, fMargin, 0);
[tableView addSubview:but];
}
}
So I go through an array of buttons (only one button in the array in my current case) and set up the frames and position them into columns and add it as a subview to the tableView. Things I've tried are:
Increase the table footer size. This just makes the gap between the table and button increase and I still can't scroll down as far as the button.
Add the button to the footer. This kind of worked. I could get to the button, but the button was always on screen, not just static below the table.
Add the button to a separate view below the table and add that view to the viewController.view. This didn't work at all and the button wasn't displayed.
Set the tableView.tableFooterView equal to the button. This also kind of worked and I could reach the button but it wouldn't work when there is more than one button in the array above.
You should create a custom view. Inside that view you should add all the buttons created using buttonArray. Now, assign this custom view to tableFooterView.
UIView *customView = [[UIView alloc] initWithFrame:properFrame];
for(int i = 0; i < [buttonArray count]; i++)
{
//...calculation of frame and other stuff
[customView addSubview:but];
}
tableView.tableFooterView = customView;
This should solve your scrolling issue.
If you want to make bottom view as stable,Take a UIView at the bottom
of UITableView.
If you will use TableFooterView,It will be at the end of cell .
In my application, I have some information in UIScrollView.
Content in UIScrollView can be changed by filter.
First time, when ViewController loads, I put all items in ScrollView, and everything working perfect.
If user wants to apply some filter to ScrollView, I remove all old items, make new, and set a new setContentSize:
- (void) displayCars:(NSMutableArray*)cars {
NSArray *viewsToRemove = [scrollView subviews];
for (UIView *v in viewsToRemove) [v removeFromSuperview];
int itemHeight = 100;
CGFloat scrollViewHeight = cars.count * itemHeight;
if (scrollViewHeight < scrollView.frame.size.height) scrollViewHeight = scrollView.frame.size.height;
[scrollView setContentSize:CGSizeMake(scrollView.frame.size.width, scrollViewHeight)];
for (int i=0; i < cars.count; i++) {
VitoCarsAdv * adv = [cars objectAtIndex:i];
VIOneCar *carView = [[VIOneCar alloc] init];
[carView setFrame:CGRectMake (0, (i * itemHeight), scrollView.frame.size.width, itemHeight)];
[carView setData:adv];
[scrollView addSubview:carView];
}
}
ScrollView is filling up by new items, but the contentSize is the same, like in previous situation, where was all items inside. Actually, it is not changed second time.
It is not comfortable behaviour for user, can somebody advice me, where I should look?
Please note, that Autolayout is swithed off for this View Controller.
Thanks.
You're doing it right I think you just mistyped some code. Here's what you wrote:
int itemHeight = 100;
CGFloat scrollViewHeight = cars.count * itemHeight;
if (scrollViewHeight < scrollView.frame.size.height) {
scrollViewHeight = scrollView.frame.size.height;
}
With that code, the scrollViewHeight will never shrink to a smaller size. You are setting a minimum size of whatever the scrollView.frame.size.height currently is. Is that your intended function? If not, it you should just remove the if statement leaving you with:
CGFloat scrollViewHeight = cars.count * itemHeight;
[scrollView setContentSize:CGSizeMake(scrollView.frame.size.width, scrollViewHeight)];
I have this code for a scrollview to showe 3 images:
const CGFloat kScrollObjHeight = 150.0;
const CGFloat kScrollObjWidth = 320.0;
const NSUInteger kNumImages = 3;
- (void)layoutScrollImages
{
UIImageView *view = nil;
NSArray *subviews = [scrollView1 subviews];
// reposition all image subviews in a horizontal serial fashion
CGFloat curXLoc = 0;
for (view in subviews)
{
if ([view isKindOfClass:[UIImageView class]] && view.tag > 0)
{
CGRect frame = view.frame;
frame.origin = CGPointMake(curXLoc, 0);
view.frame = frame;
curXLoc += (kScrollObjWidth);
}
}
// set the content size so it can be scrollable
[scrollView1 setContentSize:CGSizeMake((kNumImages * kScrollObjWidth), [scrollView1 bounds].size.height)];
}
- (void)viewDidLoad
{
self.view.backgroundColor = [UIColor viewFlipsideBackgroundColor];
// 1. setup the scrollview for multiple images and add it to the view controller
//
// note: the following can be done in Interface Builder, but we show this in code for clarity
[scrollView1 setBackgroundColor:[UIColor blackColor]];
[scrollView1 setCanCancelContentTouches:NO];
scrollView1.indicatorStyle = UIScrollViewIndicatorStyleWhite;
scrollView1.clipsToBounds = YES; // default is NO, we want to restrict drawing within our scrollview
scrollView1.scrollEnabled = YES;
// pagingEnabled property default is NO, if set the scroller will stop or snap at each photo
// if you want free-flowing scroll, don't set this property.
scrollView1.pagingEnabled = YES;
scrollView2.pagingEnabled = YES;
scrollView3.pagingEnabled = YES;
// load all the images from our bundle and add them to the scroll view
NSUInteger i;
for (i = 1; i <= kNumImages; i++)
{
NSString *imageName = [NSString stringWithFormat:#"image%d.jpg", i];
UIImage *image = [UIImage imageNamed:imageName];
UIImageView *imageView = [[UIImageView alloc] initWithImage:image];
// setup each frame to a default height and width, it will be properly placed when we call "updateScrollList"
CGRect rect = imageView.frame;
rect.size.height = kScrollObjHeight;
rect.size.width = kScrollObjWidth;
imageView.frame = rect;
imageView.tag = i; // tag our images for later use when we place them in serial fashion
[scrollView1 addSubview:imageView];
//[scrollView2 addSubview:imageView];
//[scrollView3 addSubview:imageView];
[imageView release];
}
[self layoutScrollImages]; // now place the photos in serial layout within the scrollview
}
But now I want to do a scrollview that when is in last image show me after the first image of scroll view and the same thing when I have the first image if I go back, it must show the last image; so I want create a paging loop.
The basic idea is to set yourself as a UIScrollViewDelegate and apply some modulo arithmetic to the scroll position in order to wrap it around.
There are two basic variations on the idea. Suppose your images are A, B, C, so you currently have them within the scrollview ordered as ABC.
In the more logically pleasing solution — especially if you had lots and lots of images — you watch the scroll position and as soon as it gets to a position where the view is being pushed rightward and C has left the screen, you reorder the images as CAB and shift the current scroll position one spot to the right so that the move is invisible to the user. To put that another way, the scroll position is restrained to an area of two screens, centred on the middle of B (so, you get all of B and half a screen either side). Whenever you wrap it from somewhere on the left to somewhere on the right you shift all your image views one place to the right. And vice versa.
In the slightly easier to implement variation, instead of creating a scroll view with images arranged ABC, arrange then as CABCA. Then apply the same wrap around logic but to a central area of four screens and don't do any view reshuffling.
Make sure you use just setContentOffset: (or the dot notation, as in scrollView.contentOffset =) as a setter. setContentOffset:animated: will negate velocity.