I don't really know how to explain what I want, so here's an picture :
I have a view with a lot of subviews (gray lines). Then the background (blue) is a picture (UIImageView + Blur effect), so I need it to stay and not to scroll. Behind the background, there's a view (orange). I want the picture (blue) to scroll only when the subviews (gray) are at the bottom (3rd picture).
Should I use embed scrollviews, or can I get this effect with only one UIScrollView ? If multiple scrollviews, does someone have an example ?
Thanks a lot for your help
Use only one scroll view, and use its delegate method 'scrollViewDidScroll' to read the content offset. Use this value to translate your background image.
It's not necessary to use 2 scroll views. But I would use it since I prefer setting contentOffset of the background scroll view than setting it's frame directly.
The idea is that you implement scrollViewDidScroll: delegate method for the front scroll view. And track the contentOffset to check whether the scrolling has reached the end of it's content by checking whether scrollView.contentOffset.y + scrollView.bounds.size.height > scrollView.contentSize.height or not.
If it has, then you offset the location of the background view with the amount of offset that exceed the content size.
Please see the code below. You can skip all the code and only look at scrollViewDidScroll:.
#implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.tableView.dataSource = self;
self.tableView.delegate = self;
self.backgroundScrollView.userInteractionEnabled = NO;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return 20;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"Cell"];
cell.textLabel.text = [NSString stringWithFormat:#"Item %ld", (long)indexPath.row];
return cell;
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
CGFloat backgroundVerticalOffset = MAX(scrollView.contentOffset.y + scrollView.bounds.size.height - scrollView.contentSize.height, 0);
CGPoint backgroundContentOffset = CGPointMake(0.0, backgroundVerticalOffset);
self.backgroundScrollView.contentOffset = backgroundContentOffset;
// If you decide not to use two scroll views, then please use backgroundContentOffset to set the backgroundView.frame.origin.y instead
// e.g. backgroundView.frame.origin.y = -backgroundContentOffset;
}
The result shown below:
P.S. I used blue background view instead of an image view and everything else with clear background color.
Related
I'm creating a view which comprises of a UITableView with customised cells - I'm overriding drawRect in my custom view. I've tried overriding UITableViewCell and adding my custom view as an IBOutlet, I've tried not overriding it and just referring to it by [[cell subviews] objectAtIndex:0]; both yield the same results.
When I first look at the view, all is fine. If I scroll slowly, all is fine. As soon as I scroll quickly the reused cells are clearly not re-drawing because I end up with the custom drawing, being wrong for the particular cells.
The cell configuration method...
- (UITableViewCell*) tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell* cell = [tableView dequeueReusableCellWithIdentifier:#"DialogCell"];
MaskedRoundedCornerDIalogCell* dialogCell = (MaskedRoundedCornerDIalogCell*)[[[cell contentView] subviews] objectAtIndex:0];
[dialogCell setPadding:10];
if (indexPath.row % 2 == 0) {
[dialogCell setAlignLeft:YES];
[dialogCell setMaskTopLeftOnly];
[[dialogCell textContent] setText:#"LEFT ALIGNED TEXT"];
[[dialogCell textContent] setTextAlignment:NSTextAlignmentLeft];
} else {
[dialogCell setAlignLeft:NO];
[dialogCell setMaskBottomRightOnly];
[[dialogCell textContent] setText:#"RIGHT ALIGNED TEXT"];
[[dialogCell textContent] setTextAlignment:NSTextAlignmentRight];
}
return cell;
}
My Custom drawing code in my MaskedRoundedCornerDIalogCell implementation (A class which extends UIView and is added to the UITableViewCell):
- (void) drawRect:(CGRect)rect {
int maxWidth = rect.size.width - 50;
CGRect container;
if (_alignLeft) {
container = CGRectMake(rect.origin.x + _padding, rect.origin.y + _padding, maxWidth - (2* _padding), rect.size.height - (2*_padding));
} else {
container = CGRectMake((rect.size.width - _padding) - (maxWidth - (2* _padding)), rect.origin.y + _padding, maxWidth - (2* _padding), rect.size.height - (2*_padding));
}
UIRectCorner roundedCorners;
if (!_maskTopLeft) {
roundedCorners = roundedCorners | UIRectCornerTopLeft;
}
if (!_maskTopRight) {
roundedCorners = roundedCorners | UIRectCornerTopRight;
}
if (!_maskBottomLeft) {
roundedCorners = roundedCorners | UIRectCornerBottomLeft;
}
if (!_maskBottomRight) {
roundedCorners = roundedCorners | UIRectCornerBottomRight;
}
UIBezierPath* containerBezierPath = [UIBezierPath bezierPathWithRoundedRect:container byRoundingCorners: roundedCorners cornerRadii:CGSizeMake(25.0F, 25.0F)];
[[UIColor lightGrayColor] setFill];
[containerBezierPath fillWithBlendMode: kCGBlendModeNormal alpha:1.0f];
}
The way it looks when I first launch it:
The way it looks after scrolling a few times:
Any advice, gratefully received...
I'm not sure if you're overriding drawRect in your UITableViewCell subclass or if it's in a custom UIView class. My suggestion would be to do your drawing in a custom UIView class and then add that view as a subview of your cell - just in case UITableViewCell is doing something in drawRect that you're accidentally overriding.
In any case, the reason you're seeing this behaviour is because drawRect is only called when the view first comes on the screen or if it's invalidated. From the docs:
This method is called when a view is first displayed or when an event occurs that invalidates a visible part of the view. You should never call this method directly yourself. To invalidate part of your view, and thus cause that portion to be redrawn, call the setNeedsDisplay or setNeedsDisplayInRect: method instead.
In your cell's setMask.. methods, try calling [self.customDrawingView setNeedsDisplay] to invalidate the drawing and force an update.
You can use this method or the setNeedsDisplayInRect: to notify the system that your view’s contents need to be redrawn. This method makes a note of the request and returns immediately. The view is not actually redrawn until the next drawing cycle, at which point all invalidated views are updated.
You should use this method to request that a view be redrawn only when the content or appearance of the view change. If you simply change the geometry of the view, the view is typically not redrawn. Instead, its existing content is adjusted based on the value in the view’s contentMode property. Redisplaying the existing content improves performance by avoiding the need to redraw content that has not changed.
At first, I don't think, that overriding - (void)drawRect:(CGRect)rect is the right resolution for your problem. My advice would be to override it only if you exactly know what are you doing and what are you trying to achieve.
According to images, you have provided, the problem you are facing is related with UITableViewCell reusing, but not with the drawing.
The problem. When table view scrolling hits the boundary of UITableView to present new content, UITableView doesn't create a new cell instance. It takes the currently invisible cell (the one which has been to scrolled out to present new one on screen) and reuses it (UITableViewCell* cell = [tableView dequeueReusableCellWithIdentifier:#"DialogCell"];). In your example, lines of code executed after the cell is being reused only sets the cell content text up. From the images you provided, I see, that the text is sat up correctly. The problem is with cells background. Since there is not a single line of code to setup cells background after it is being reused, background before reuse is being presented.
Recommendations. Obviously, you need to override cells background each time it is being reused. If I were you, I would create a subclass of class UITableViewCell, lets say MyTableViewCell. The cell would have a method like:
- (void)setupWithSide:(Side)side {
if (side == SideRight) {
[self setupRightAlignedCintent];
[self setupLeftBackground];
}
}
Meanwhile, in UITableViewController you have to
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
MyTableViewCell *cell = (MyTableViewCell *)[tableView dequeueReusableCellWithIdentifier:#"DialogCell"];
Side side;
if (indexPath.row % 2 == 0) {
side = SideRight;
} else {
side = SideLeft;
}
[cell setupWithSide:side];
return cell;
}
I have a simple UITableView setup and have a footer view which has just a UIActivityIndicatorView in it.
In viewDidLoad I call:
[_tableView registerNib:[UINib nibWithNibName:#"FetchingResultsFooterView" bundle:nil] forHeaderFooterViewReuseIdentifier:#"TableViewFooterFetchingResultsView"];
Then I add the tableview methods for my footer:
-(CGFloat)tableView:(UITableView *)tableView heightForFooterInSection:(NSInteger)section
{
return [self tableViewFooterHeight];
}
-(CGFloat)tableViewFooterHeight
{
return 88;
}
-(UIView *)tableView:(UITableView *)tableView viewForFooterInSection:(NSInteger)section
{
static NSString *headerReuseIdentifier = #"TableViewFooterFetchingResultsView";
APFetchingResultsFooter *footer = [tableView dequeueReusableHeaderFooterViewWithIdentifier:headerReuseIdentifier];
[footer setTag:kTableViewFooterTag];
//Centre my activity indicator on screen
CGFloat aiInset = ((_tableViewContainer.frame.size.width /2) - (footer.activityIndicator.frame.size.width /2));
CGFloat aiOffset = _tableViewContainer.contentOffset.x + aiInset;
[footer.activityIndicator setFrame:CGRectMake(aiOffset, ([self tableViewFooterHeight] /2) - (footer.activityIndicator.frame.size.height /2), footer.activityIndicator.frame.size.width, footer.activityIndicator.frame.size.height)];
return footer;
}
This displays my footer more or less as expected, it sits there with my activity indicator.
In case it confuses you, because I have a very wide 'UITableView' inside a 'UIScrollView' I simply centre my activity indicator on screen, rather than the centre of the tableview by offsetting it.
Which leads to this code which just updates the position of the activity indicator as I scroll, so it stays centred (allowing the tableview wider than the screen to use the footer).
-(void)scrollViewDidScroll:(UIScrollView *)scrollView
{
//Synchronise scrolling
if (scrollView == _headerView) [_tableViewContainer setContentOffset:_headerView.contentOffset];
else if (scrollView == _tableViewContainer) [_headerView setContentOffset:_tableViewContainer.contentOffset];
//because the tableview can be wider than the screen, we adjust the footer contents to be centered as we scroll
if (scrollView == _tableViewContainer)
{
APFetchingResultsFooter *footer = (APFetchingResultsFooter *)[_tableView viewWithTag:kTableViewFooterTag];
CGFloat aiInset = ((_tableViewContainer.frame.size.width /2) - (footer.activityIndicator.frame.size.width /2));
CGFloat aiOffset = _tableViewContainer.contentOffset.x + aiInset;
[footer.activityIndicator setFrame:CGRectMake(aiOffset, ([self tableViewFooterHeight] /2) - (footer.activityIndicator.frame.size.height /2), footer.activityIndicator.frame.size.width, footer.activityIndicator.frame.size.height)];
}
}
So, that is all pretty simple. Now my problem:
As I scroll vertically, when the footer is offscreen and then brought back on-screen, the footer is reset. So the activity indicator is not centred.
I have tried placing the frame update code in cellForRowAtIndexPath which kind of works, but as you scroll the footer on-screen, there is a delay and you see the indicator jump from its default frame to its new centred one.
How do I prevent this, so that as it is scrolled on screen (or as the footer is created), have the indicator view updated with a new frame before any jumping can be seen?
Have I just taken the wrong approach?
I try to make UICollectionView with cells, that intersect and partially overlay each other as it is done at screenshot:
This layout was reached by setting
self.minimumLineSpacing = -100;
at my UICollectionViewFlowLayout subclass.
When I scroll down, everything is OK. I see what I want. But when I scroll up, I see another behaviour, not like I expected:
So my question is: how can I make my layout look as at the first screen regardless scroll view direction.
Note: I have to support both iOS 6 and 7.
Thanks very much for any advices and any help.
Hmm, interesting. Since the collection view recycles cells, they are continuously added to and removed from the view hierarchy as they move on and off the screen. That being said, it stands to reason and when they are re-added to the view, they are simply added as subviews meaning that when a cell gets recycled, it now has the highest z-index of all of the cells.
One fairly pain-free way to rectify this would be to manually adjust the z position of each cell to be incrementally higher with the index path. That way, lower (y) cells will always appear above (z) the cells above (y) them.
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *cellID = #"CELLID";
UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:cellID forIndexPath:indexPath];
if (cell.layer.zPosition != indexPath.row) {
[cell.layer setZPosition:indexPath.row];
}
return cell;
}
Found another sollution to solve this problem. We need to use UICollectionViewFlowLayout subclass.
#interface MyFlowLayout : UICollectionViewFlowLayout
#end
#implementation MyFlowLayout
- (void)prepareLayout {
[super prepareLayout];
// This allows us to make intersection and overlapping
self.minimumLineSpacing = -100;
}
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{
NSArray *layoutAttributes = [super layoutAttributesForElementsInRect:rect];
for (UICollectionViewLayoutAttributes *currentLayoutAttributes in layoutAttributes) {
// Change zIndex allows us to change not only visible position, but logic too
currentLayoutAttributes.zIndex = currentLayoutAttributes.indexPath.row;
}
return layoutAttributes;
}
#end
Hope that helps someone else.
I have a custom cell shown in attachment:
I am using a UIWebView to display tweet text. Some of the tweets are short and I want to resize the web view, move up the Time Since, and then finally resize the cell. I know how to resize the web view, but don't know how to figure out how to move the time since and resize the cell.
- (CGFloat) tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
NSFetchedResultsController *fetchedResultsController = nil;
if (self.tweetsSegmentedControl.selectedSegmentIndex == REPORTERS_SELECTED_INDEX)
{
fetchedResultsController = self.reportersFetchedResultsController;
}
else if (self.tweetsSegmentedControl.selectedSegmentIndex == PLAYERS_SELECTED_INDEX)
{
fetchedResultsController = self.playersFetchedResultsController;
}
Tweet *tweet = [fetchedResultsController objectAtIndexPath:indexPath];
//Calcaulate height of box, instead of return a fixed number
return 170;
}
I know how to resize the web view
Don't. Use autosizing to stretch the UIWebView with height of the cell.
how to move the time since
Don't. Use autosizing to pin Time Since to the bottom of the cell.
resize the cell
Use -tableView:heightForRowAtIndexPath:
Update
The way I did this before is to set a known size for the flexible view (for you the web view) in the prototype cell and set a known size for the prototype cell. In this case we'll say the web view is kPrototypeViewHeight and the prototype is kPrototypeCellHeight.
In the table view controller:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
UIWebView *webView = … // Whatever you need to do to get the web view
CGFloat webViewHeight = webView.frame.size.height;
return kPrototypeCellHeight - kPrototypeViewHeight + webViewHeight;
}
Note: This will be done before -tableView:cellForRowAtIndexPath: is called. So you need to know your size before you have to draw yourself.
Note 2: Because you are changing the size of webView yourself, you will not want it autosized.
Note 3: You will be redering each web view in your table to calculate your height. My guess is scrolling will be very slow.
Hope that helps.
I am trying to resize my UITableViewCell's frame via:
[cell setFrame:CGRectMake(cell.frame.origin.x,
cell.frame.origin.y,
cell.frame.size.width,
cell.frame.size.height+25)];
however, it's not resizing after I do this... why is this?
This is weird as if I add a UIToolBar into the cell, it resizes but when I am adding a UIView, it doesn't:
[cell.contentView addSubview:sideSwipeView];
Here's the long and short of it:
Your cell width is determined by the width of the tableview it's in.
[EDIT: If it's a grouped table view, the cell is 20 - 60 pixels narrower than the tableview width, depending if you're using an iPhone, or an iPad.]
Your cell height is determined by the heightForRowAtIndexPath method.
If you're manually setting the cell's frame, it's going to be useless except when you're using a subclassed cell where you want to add subviews based on the cell's dimensions.
Even in this case, it's recommended to get the cell's frame from the tableview by using rectForRowAtIndexPath:(NSIndexPath*)indexPath method and then setting that frame as the cell's frame (after setting the frame's origin Y as 0).
I'm not quite sure about the UIToolBar, but your subview's frame won't change on changing the cell frame.
Maybe if you could tell us what you're trying to achieve, we can suggest a solution for you?
--------------------EDIT--------------------
So you need to dynamically add a subview to a cell on tapping it and resize it's height according to the new subview. This is gonna get hairy so here goes:
In your .h file declare:
BOOL subviewAdded;
In your .m file, in the init, do:
subviewAdded = NO;
Let's assume that you want the cell's height to be 50 without the subview and 100 with the subview. Accordingly, your heightForRow method should be:
- (CGFloat)tableView:(UITableView *)tableView
heightForRowAtIndexPath:(NSIndexPath *)indexPath {
return (subviewAdded?100.0f:50.0f);
}
This means that initially since subviewAdded is NO, all your cells will have the smaller height.
Now, to add a subview to a cell on tapping it, and to change it's height dynamically, do this in your didSelectRow method:
- (void)tableView:(UITableView *)tableView
didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
//Get the cell at this indexPath
UITableViewCell *thisCell = [tableView cellForRowAtIndexPath:indexPath];
if(subviewAdded)
{
subviewAdded = NO;
for(int i = 0; i < [thisCell.contentView.subviews count]; i++)
{
UIView *thisSubview = [thisCell.contentView.subviews objectAtIndex:i];
[thisSubview removeFromSuperview];
}
}
else
{
UIView *someView = [[UIView alloc] initWithFrame:someFrame];
[thisCell.contentView addSubview:someView];
[someView release];
subviewAdded = YES;
}
NSMutableArray *array = [NSMutableArray array];
[array addObject:indexPath];
[tableView reloadRowsAtIndexPaths:array
withRowAnimation:UITableViewRowAnimationFade];
}
So what's going to happen here is you're adding a subview to this cell you've tapped. Reloading this cell will call heightForRowAtIndexPath and do a nice little fade animation and change your tableview heights.
IMPORTANT: Ideally, you should maintain an array of NSNumbers with boolean values. The array size should be the same size as the number of tableview cells you have.
In heightForRow, you would then check against this array instead of using a single boolean for the whole tableView. This would ensure that you could have different heights for different cells.
That would look something like:
- (CGFloat)tableView:(UITableView *)tableView
heightForRowAtIndexPath:(NSIndexPath *)indexPath {
BOOL thisBool = (BOOL)[[booleanArray objectAtIndex:indexPath.row] boolValue];
return (thisBool?100.0f:50.0f);
}
I didn't post all that code here since it's implied and what I've posted should put you well on your way to doing the boolean array thing.
Anyway, there you are. I just tested this code myself so it works :)
If you want to increase the height of your cell based on some parameter eg. text, image,
you must implement the heightForRowAtIndexPath method of UITableViewDelegate in your code.
- (CGFloat)tableView:(UITableView *)tableView
heightForRowAtIndexPath:(NSIndexPath *)indexPath