I have a grouped UITableView with custom cells (created by subclassing UITableViewCell). I add subviews and insert sublayers just like this:
[self.contentView addSubview:myUILabel];
and
[self.contentView.layer insertSublayer:myCALayer];
When entering editing mode for deleting rows, the cells move right and myUILabel and myCALayer go beyond the borders of the cell, which looks ugly.
I tried this:
Grouped UITableView with custom UILabels in Editing Mode
... but it didn't help.
What "kind of" worked is to override setEditing: in my custom cell
- (void)setEditing:(BOOL)editing animated:(BOOL)animated {
CGRect oldBounds = self.myCALayer.bounds;
CGRect newBounds = oldBounds;
CGPoint oldPosition = self.myCALayer.position;
CGPoint newPosition = oldPosition;
//move right
if (editing && !self.showingDeleteConfirmation) {
newBounds.size.width -=32;
newPosition.x -= 15;
}
//move back left
else if (self.editing) {
newBounds.size.width += 32;
newPosition.x += 15;
}
self.myCALayer.bounds = newBounds;
self.myCALayer.position = newPosition;
[super setEditing:editing animated:animated];
}
... however, the animations are not performed synchronously. What happens is while the cell moves right when entering editing mode, first the width of the layer shrinks, and then the position changes. Although the layer finally fits the cell, the animation looks bad.
Thanks for your advice!
Not sure about your layers, but you should be able to set a smart autoresizingMask on your subviews so they re-layout appropriately when the size of contentView is altered by the tableview (on entry to edit mode).
More here.
Edit: with a little looking, I think CALayer's anchorPoint property may help with your sublayers. (more here)
Alternatively (and this is a bit of a hack, and could hurt performance somewhat), you could set your custom layers on a UIView w/ an appropriately set autoresizingMask.
Related
I am currently testing in xcode a UICollectionView with just one horizontal row like a kinda cover flow. Basically I have my own Custom Cell class and xib file for the cell and then on each cell I am adding another UIView with a xib. In case you are wondering why, it is so I can add different UIViews to the cell. Right now I am only adding one.
Edit I have followed the WWDC 2012 video on creating a linelayout of a UICollectionViewCell with one difference. Instead of the cell in the middle getting bigger all the other cells get smaller.
Everything below is new to my question.
-(NSArray *)layoutAttributesForElementsInRect:(CGRect)rect {
NSArray *array = [super layoutAttributesForElementsInRect:rect];
CGRect visibleRect;
visibleRect.origin = self.collectionView.contentOffset;
visibleRect.size = self.collectionView.bounds.size;
for (UICollectionViewLayoutAttributes *attributes in array){
if (CGRectIntersectsRect(attributes.frame, rect)) {
CGFloat distance = CGRectGetMidX(visibleRect) - attributes.center.x;
CGFloat normalizedDistance = distance / ACTIVE_DISTANCE;
if (ABS(distance) < ACTIVE_DISTANCE) {
//THIS WOULD MAKE THE MIDDLE BIGGER
//CGFloat zoom = 1 + ZOOM_FACTOR *(1- ABS(normalizedDistance));
//attributes.transform3D = CATransform3DMakeScale(zoom, zoom, 1.0);
//attributes.zIndex = round(zoom);
} else {
//THIS MAKES ALL THE OTHERS NOT IN THE RECT SMALLER
CGFloat zoom = 1 + ZOOM_FACTOR *(1- ABS(normalizedDistance));
attributes.transform3D = CATransform3DMakeScale(zoom, zoom, 1.0);
attributes.zIndex = round(zoom);
}
}
}
return array;
}
The problem can be seen in the attached image.
Pink = Collection View Size
Brown = Cell size
Green = Cells Content size and an attached xib to the content size.
The problem I THINK I have is with the layout. When the sell is dequeued it is made smaller by the above code. Then when it is reused the CELL gets bigger again but the content view does not.
I have tired to manually set the frame of the content view but that does nothing.
UPDATE 1: This also only happens when I add a xib to the Cells content view. If there is no subview to the content view then there is no problem
UPDate 2: It appears that the subview of the cell, my xib is not resizing. I have tried to manually change its frame size but the only place this helps is in the cells drawrect method which feels like a hack to me.
reused cell not able to redraw itself so give call to
-(void)setFrame:(CGRect)frame {
[super setFrame:frame];
[self setNeedsDisplay]; // force drawRect:
}
from cellForItemAtIndexPath of the UICollectionView.
also have a look at this link
and this question
My answer is very specific and I am not sure it will help anyone.
The problem was that I had a constraint on the bottom of the grey view. After I changed this constraint to a less than or equal too then for some reason it worked.
Now I know this does not explain why it was not happening to every cell but it fixed my problem.
As such Harsh's answer might also be worth looking at if you have landed here after doing a search.
Edit: there also appears to be some bugs in the 6.0 UiCollectionView controller which seem to be fixed in 6.1
I want to add a subview in the top of my view, I have to recalculate the origin y value for all of other views and re-position them to leave space for the new added view.
It is very boring, as I know android have relativelayout or linearlayout can help automatically do that.
How to solve this problem easily in ios development?
I've created a library to solve just this problem: CSLinearLayoutView
You use it like this:
// create the linear layout view
CSLinearLayoutView *linearLayoutView = [[[CSLinearLayoutView alloc] initWithFrame:self.view.bounds] autorelease];
linearLayoutView.orientation = CSLinearLayoutViewOrientationVertical;
[self.view addSubview:linearLayoutView];
// create a layout item for the view you want to display and add it to the layout view
CSLinearLayoutItem *item = [CSLinearLayoutItem layoutItemForView:someView];
item.padding = CSLinearLayoutMakePadding(5.0, 10.0, 5.0, 10.0);
item.horizontalAlignment = CSLinearLayoutItemHorizontalAlignmentCenter;
item.fillMode = CSLinearLayoutItemFillModeNormal;
[linearLayoutView addItem:item];
// add more items
I've been trying to do a relative (linear) layout for a while and finally decided to just subclass UIScrollView to get it done.
I started out just replacing layoutSubviews with a simple loop through the subviews that reset the origins while keeping a running Y. But, some unexpected things are added to the scrollview, including UIInlineAutoCorrect views from textfields/views, which means these things were being mangled by the layout. So I added a little bit of logic that uses the tag property of a UIView to determine if I should lay it out:
-(void) layoutSubviews{
CGFloat runningY = 0.0f;
CGFloat widestWidth = 0.0f;
for (UIView *view in self.subviews) {
if (view.tag != 1999) {
continue;
}
view.origin = CGPointMake(view.origin.x, runningY);
runningY += view.height;
if ([view autoresizingMask] == UIViewAutoresizingFlexibleWidth) {
view.width = self.width;
}
if (view.width > widestWidth) {
widestWidth = view.width;
}
}
[self setContentSize:CGSizeMake(widestWidth, runningY)];
}
If you would still like to use unique tags for your views, you should just specify a range of tags that will be included in the layout instead of a single value.
It's not much work to subclass UIView to make sense of methods like -(void)addView:toRightOfView: etc. You could do this as you go, porting only the methods you need. You could then call these in your override of layoutSubviews as Benjamin indicates.
Views can be built using IB or they can be written programmatically; Android scores well here in making layouts readable and you can bring that benefit to iOS views created programmatically. That there are few iOS devices means beyond readability there are not (yet?) many practical benefits to this pattern.
NB. A "XIB" file is an XML file. Open it up in your favourite text editor and take a look.
** EDIT.
Here's a quick example I knocked up. It has not been tested but some thing like this will work in your subclass of UIView (call it UIRelativeView perhaps).
- (void) addSubview:(UIView *) viewOne
toRightOfSubview:(UIView *) viewTwo
{
if (viewTwo == nil ||
[self.subviews contains:viewTwo] == NO)
{
[self addSubview:viewOne];
}
else
{
CGRect frameTwo = viewTwo.frame;
CGPoint originOne = CGPointMake(frameTwo.origin.x + frameTwo.size.width,
frameTwo.origin.y);
CGRect frameOne = CGRectZero;
frameOne.origin = originOne;
frameOne.size = viewOne.frame.size;
[viewOne setFrame:frameOne];
[self addSubview:viewOne];
}
}
- (void) moveSubview:(UIView *) viewOne
toRightOfSubview:(UIView *) viewTwo
{
if (viewTwo == nil ||
[self.subviews contains:viewTwo] == NO)
{
[self addSubview:viewOne];
}
else if ([self.subviews contains:viewOne] == NO)
{
[self addSubview:viewOne toRightOfSubview:viewTwo];
}
else
{
CGRect frameTwo = viewTwo.frame;
CGPoint originOne = CGPointMake(frameTwo.origin.x + frameTwo.size.width,
frameTwo.origin.y);
CGRect frameOne = CGRectZero;
frameOne.origin = originOne;
frameOne.size = viewOne.frame.size;
[viewOne setFrame:frameOne];
}
}
You've got no luck here. iOS doesn't have provisions for positioning the views in different layouts like Android. You need to reposition all the other subviews to make the way for the new view.
There are some view resizing methods like sizeToFit and autoResizingMask but they won't help you in your case here.
iOS is much more focused on pixel accuracy than Android it is, which uses relative layouts as it has to deal with multiple screen sizes. However, in iOS, the Interface Builder is an incredibly good tool included in XCode, which you can use.
Also, if you are just adding subviews in a repetitive manner, you could override the layoutSubviews method and use that to handle to manual labour for you. You mention having to "recalculate the origin y value for all of other views and re-position them to leave space for the new added view" ... You could code that into your layoutSubviews so you don't have to do it yourself each time.
Unfortunately, though, the SDK doesn't have any of this included by default. autoresizingMask's are great but you can't use that for initial layout; it's for automatic really it when rotating only.
As of iOS 9 you can use UIStackView, which works very similarly to LinearLayout: you add views and the stack view arranges them as needed based on your sizing preferences:
Fill will leave three of them their natural size, and make the fourth one take up the most space. It uses Auto Layout's content hugging priority to decide which one to stretch.
Fill Equally will make each subview the same size so they fill all the space available to the stack view.
Fill Proportionally uses the intrinsic content size of each subview to resize them by an equal amount. So view 1 was designed to have twice as much height as views 2, 3 and 4, that ratio will remain when they are resized – all the subviews get proportionally bigger or smaller.
Equal Spacing does not resize the subviews, and instead resizes the spacing between the subviews to fill the space.
Equal Centering is the most complicated, but for many people also the most aesthetically pleasing. It attempts to ensure the centers of each subview are equally spaced.
You can also set spacing between views in the stack view, adding some padding.
WARNING: When adding stack view child views in code you should always use addArrangedSubview() like this:
stackView.addArrangedSubview(someView)
If you try to use plain old addSubview() it won't work correctly, because the stack view won't know to arrange it.
As for removing, you need to be careful to use stackView.removeArrangedSubview(someView) and someView.removeFromSuperview() otherwise the view won't be removed correctly.
You might find my UIStackView tutorial useful.
I have the UIScrollView with pagingEnabled set to YES, and programmatically scroll its content to bottom:
CGPoint contentOffset = scrollView.contentOffset;
contentOffset.y = scrollView.contentSize.height - scrollView.frame.size.height;
[scrollView setContentOffset:contentOffset animated:YES];
it scrolls successfully, but after that, on single tap its content scrolls up to offset that it has just before it scrolls down. That happens only when I programmaticaly scroll scrollView's content to bottom and then tap. When I scroll to any other offset and then tap, nothing is happened.
That's definitely not what I'd like to get. How that should be fixed?
Much thanks in advance!
Timur.
This small hack prevents the UIScrollView from scrolling when tapped. Looks like this is happening when the scroll view has paging enabled.
In your UIScrollView delegate add this method:
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
scrollView.pagingEnabled = self.scrollView.contentOffset.x < (self.scrollView.contentSize.width - self.scrollView.frame.size.width);
}
This disables the paging when the scroll view reaches the right end in horizontal scrolling (my use case, you can adapt it to other directions easily).
I just figured out what causes this problem, and how to avoid it. If you having pagingEnabled set to YES on a scroll view, you must set the contentOffset to be a multiple of the scroll view's visible size (i.e. you should be on a paging boundary).
Concrete example:
If your scroll view was (say) 460 pixels high with a content area of 920, you would need to set the content offset to EITHER 0 or 460 if you want to avoid the "scroll to beginning on tap" problem.
As a bonus, the end result will probably look better since your scroll view will be aligned with the paging boundaries. :)
The following workaround did help (assume that one extends UIScrollView with a category, so 'self' refers to its instance):
-(BOOL) scrolledToBottom
{
return (self.contentSize.height <= self.frame.size.height) ||
(self.contentOffset.y == self.contentSize.height - self.frame.size.height);
}
Then, one should sometimes turn pagingEnabled off, just at the position where scroll view reaches its bottom. In the delegate (pagingEnabled is initialy on of course, since the problem occurs only when it is enabled):
-(void) scrollViewDidScroll:(UIScrollView *)scrollView
{
if (scrollView.pagingEnabled == YES)
{
if ([scrollView scrolledToBottom] == YES)
scrollView.pagingEnabled = NO;
}
else
{
if ([scrollView scrolledToBottom] == NO)
scrollView.pagingEnabled = YES;
}
}
This seems to be a bug:
UIScrollView doesn't remember the position
I have tested this on iOS 4.2 (Simulator) and the issue remains.
When scrolling a ScrollView I would suggest using
[scrollView scrollRectToVisible:CGRectMake(0,0,1,1) animated:YES];
Where the rect is the position you're after. (In this case the rect would be the top of the scrollview).
Changing the content offset is not the correct way of scrolling a scrollview.
My app's table view does not occupy the full screen height, as I've allowed 50px at the bottom for a banner.
When I begin typing in the search bar, the search results table view is larger; it fills all available screen space between the search bar and the tab bar. This means that the very last search result is obscured by the banner.
How do I specify the size of the table view used by UISearchDisplayController? There's no bounds or frame property that I can see.
EDIT TO ADD SCREENSHOTS:
This is how the table view is set up in IB. It ends 50px short of the synthesized tab bar.
(source: lightwood.net)
This is how content displays normally. I've scrolled to the very bottom here.
(source: lightwood.net)
This is how it displays when searching. Again, I've scrolled to the very bottom. If I disable the banner ad, I can see that the search display table spreads right down to the tab bar.
(source: lightwood.net)
The key to solving this one was finding out when to change the geometry of the table view. Calling:
[self.searchDisplayController.searchResultsTableView setFrame:someframe];
after creating the UISearchDisplayController was futile. The answer was this delegate method:
-(void)searchDisplayController:(UISearchDisplayController *)controller didShowSearchResultsTableView:(UITableView *)tableView {
tableView.frame = someframe;
}
Note, I had also tried -searchDisplayController:didLoadSearchResultsTableView but it did no good in there. You have to wait until it's displayed to resize it.
Also note that if you simply assign tableView.frame = otherTableView.frame, the search results table overlaps its corresponding search bar, so it is impossible to clear or cancel the search!
My final code looked like this:
-(void)searchDisplayController:(UISearchDisplayController *)controller didShowSearchResultsTableView:(UITableView *)tableView {
CGRect f = self.masterTableView.frame; // The tableView the search replaces
CGRect s = self.searchDisplayController.searchBar.frame;
CGRect newFrame = CGRectMake(f.origin.x,
f.origin.y + s.size.height,
f.size.width,
f.size.height - s.size.height);
tableView.frame = newFrame;
}
I updated the code to allow for deeper view hierarchies, but the initial frame of the semi-transparent cover view still takes up the entire window below the search bar.
-(void)searchDisplayController: (UISearchDisplayController*)controller
didShowSearchResultsTableView: (UITableView*)tableView
{
if ( [controller.searchBar.superview isKindOfClass: [UITableView class]] )
{
UITableView* staticTableView = (UITableView*)controller.searchBar.superview;
CGRect f = [tableView.superview convertRect: staticTableView.frame fromView: staticTableView.superview];
CGRect s = controller.searchBar.frame;
CGRect newFrame = CGRectMake(f.origin.x,
f.origin.y + s.size.height,
f.size.width,
f.size.height - s.size.height);
tableView.frame = newFrame;
}
}
I'm not sure I completely understand what you're describing, it would be nice to have a screenshot.
It sounds like what's happening is the UITableView is the size of the screen and the banner is overlapping the bottom 50 pixels of it. All UIView children have a frame, bounds, and center properties they inherit from their common UIView parent.
#interface UIView(UIViewGeometry)
// animatable. do not use frame if view is transformed since it will not correctly reflect the actual location of the view. use bounds + center instead.
#property(nonatomic) CGRect frame;
// use bounds/center and not frame if non-identity transform. if bounds dimension is odd, center may be have fractional part
#property(nonatomic) CGRect bounds; // default bounds is zero origin, frame size. animatable
#property(nonatomic) CGPoint center; // center is center of frame. animatable
#property(nonatomic) CGAffineTransform transform; // default is CGAffineTransformIdentity. animatable
// ... from UIView.h
You can manipulate these properties as you like, it sounds like you simply need to adjust the bounds to be 50 pixels smaller than the screen, and do some math to calculate your new center.
So, UITableView supports essentially "infinite" scrolling. There' may be a limit but that sucker can scroll for a looonnnggg time. I would like to mimic this behavior with a UIScrollView but there are two fundamental impediments:
1) scrollView.contentSize is fixed at creation time.
2) zooming can blow any lazy-loading scheme all to hell since it can cause infinte data explosion.
Have others out there pondered this idea? Yah, I know, we are essentially talking about re-creating Google Maps here. Any insights would be much appreciated.
Cheers,
Doug
I've just finished implementing the infitine scroll for me.
In my Implementation I have UITableViewCell with a scrollView and Navigationbuttons. The scrollView contains x views all with the same width. views are alined horizontally and paging is enabled.
scrollView.clipsToBounds = YES;
scrollView.scrollEnabled = YES;
scrollView.pagingEnabled = YES;
scrollView.showsHorizontalScrollIndicator = NO;
My codelogic is like the following:
In my initialization function I
create all the views (for the scrollview) and
put them into an array and
add them to the scrollView
Then I call a function that calculates in a loop the positions for each view (each time you detect a scroll this function will need to be called too). It always takes the first element of the array and sets the frame to (0,0,...,...), the second with (i*width,0,....,....) and so on. The function beeing called looks like this:
- (void)updateOffsetsOfViews{
int xpos = 0;
for (int i=0; i<[views count]; i++) {
UIImageView *_view = [views objectAtIndex:i];
CGRect aFrame = _view.frame;
aFrame.origin.x = xpos;
aFrame.origin.y = 0.0;
_view.frame = aFrame;
xpos += viewWidth;
}
float center = 0;
if(fmod([views count],2) == 1){
center = viewWidth * ([views count]-1)/2;
}else {
center = viewWidth * [views count]/2;
}
[scrollView setContentOffset:CGPointMake(center, 0)];
lastOffset = center;
}
Then (still in the initialization process) I add an observer
[scrollView addObserver:self forKeyPath:#"contentOffset" options:0 context:nil];
so each time something in the scrollView changes I get the (observeValueForKeyPath)-function called, which looks like this:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
UIImageView *_viewFirst = (UIImageView *)[views objectAtIndex:0];
if ( fmod([scrollView contentOffset].x,viewWidth) == 0.0) {
if ([scrollView contentOffset].x > lastOffset) {
[views removeObjectAtIndex:0];
[views addObject:_viewFirst];
[self updateOffsetsOfViews];
}else if ([scrollView contentOffset].x < lastOffset) {
UIImageView *_viewLast = (UIImageView *)[views lastObject];
[views removeLastObject];
[views insertObject:_viewLast atIndex:0];
[self updateOffsetsOfViews];
}
}
}
And in dealloc or viewDidUnload (depends on how you implement it) don't forget to remove the observer.
[scrollView removeObserver:self forKeyPath:#"contentOffset"];
Hope this helps, you might notice some overhead, but in my implementation I also support like scrolling 5 pages (well... unlimited) at once and autoanimated scrolling etc. so you might see something that could be thrown away.
While it's impossible to have a truly infinite UIScrollView, there are some simple tricks you can use to emulate that behavior.
Handling the fixed contentSize: have some fixed-size view handled by your scroll view, and at launch or instantiation, set the content offset so that you're seeing the middle of the handled view. Then just watch the content offset (using KVO or some other method), and if you near any edge, update the content of the view with a new set of content (offset appropriately) and reset the scroll view's contentOffset property to be back in the middle.
Handling zooming: do something similar, only this time watch the zoom factor on the scroll view. Whenever it gets to a certain point, do some manipulation to whatever data you're presenting so that it appears zoomed, then reset the zoom factor to 1.0. For example, if you're scrolling an image and it gets zoomed to appear twice as large, programmatically apply some kind of transform to make the image twice as large, then reset the scroll view's zoom factor to 1.0. The image will still appear zoomed in, but the scroll view will be able to continue zooming in further as necessary. (Google Maps takes this one step further where it lazy-loads more detailed views as the user zooms - you may or may not choose to implement this.)
The StreetScroller sample project from Apple demonstrates how to perform infinite scrolling in a UIScrollView.
Bear in mind that when the scroll is animated, contentOffset changes many times, not just page by page, but with each step in the animation.
Perhaps setting contentSize to some gigantic value and then moving a limited number of underlying views around to track the view position as in the Tiling sample will do the trick.
To mitigate the possibility of eventually reaching an edge and having to abruptly recenter the view (which cancels any scrolling currently in motion), the view can be recentered when it is stationary, from time to time.
Anyway, that's what I'm about to try.