iOS: determine when all children have had layoutSubviews called - ios

it appears that viewDidLayoutSubviews is called immediately after layoutSubviews is called on a view, before layoutSubviews is called on the subviews of that view. Is there any way of knowing when layoutSubviews has been called on a view and all of its children that also needed their layouts updated?

You shouldn't have to know if the subviews of a subview have updated their layout: That sounds like too tight coupling. Also, each subview might handle the arrangement of their respective subviews differently and might not (need to) call layoutSubviews for its subviews at all. You should only ever have to know about your direct subviews. You should treat them more or less as black boxes and not care whether they have subviews of their own or not.

As #Johannes Fahrenkrug said, you should "treat them as black boxes". But according to my understandings, it is because that Cocoa just can't promise it.
If you really need to be notified when all subviews have done the layout job, here is a hardcore sample may solve your problem. I don't either promise it would work under every situation.
- (void) layoutSubviewsIsDone{
// Your code here for layoutSubviews is done
}
// Prepare two parameters ahead
int timesOfLayoutSubviews = 0;
BOOL isLayingOutSubviews = NO;
// Override the layoutSubviews function
- (void) layoutSubviews{
isLayingOutSubviews = YES; // It's unsafe here!
// you may move it to appropriate place according to your real scenario
// Don't forget to inform super
[super layoutSubviews];
}
// Override the setFrame function to monitor actions of layoutSubviews
- (void) setFrame:(CGRect)frame{
if(isLayingOutSubviews){
if(frame.size.width == self.frame.size.width
&& frame.size.height == self.frame.size.height
&& frame.origin.x == self.frame.origin.x
&& frame.origin.y == self.frame.origin.y
&& timesOfLayoutSubviews ==self.subviews.count){
isLayingOutSubviews = NO;
timesOfLayoutSubviews = 0;
[self layoutSubviewsIsDone]; // Detected job done, call your function
}else{
timesOfLayoutSubviews++;
}
}

Related

Create round UIButton with custom drawRect: method

My goal is to create a custom UIButton subclass, that creates a round (or round rect) button with some additional features.
After some searching, I found that simply setting the cornerRadius of the button layer is the easiest way to make the button round:
#implementation MyRoundButton
-(id)initWithFrame:(CGRect)frame{
if (self = [super initWithFrame:frame])
[self setupView];
return self;
}
-(id)initWithCoder:(NSCoder *)aDecoder{
if(self = [super initWithCoder:aDecoder])
[self setupView];
return self;
}
-(void)setupView {
self.layer.cornerRadius = MIN(self.frame.size.height, self.frame.size.width) / 2.0;
}
This works fine to, as long as I do not override drawRect:
- (void)drawRect:(CGRect)rect {
[super drawRect:rect];
}
Of course I would like to add some custom drawing code to drawRect: but even if only [super drawRect:rect] the button is not round anymore: It drawn as rectangle within its bounds.
How is this possible? How can overriding a method with a simple super call change the behavior at all?
How can I avoid this problem? I already tried to let the layer unchanged and to simply draw the background manually in drawRect:. However the button draws its background rect anyway and my custom drawing is on top of it.
So, how to solve this?
The layer in your example conceptually contains a background with a corner radius, but the empty override of drawRect: causes that to be ignored. That's because, ordinarily, an instance of UIView depends on its built-in layer (an instance of CALayer) to render its content, so drawRect: isn't called. However, the layer is capable of delegating drawing behavior to its view, and will do so if you implement drawRect:.
If you're simply subclassing UIView, this shouldn't present any problem. However, subclassing a framework component such as UIButton is a bit dicier. One potential issue is the call to -[super drawRect:]; it's hard to know precisely what mischief that might cause, but that may be the source of the problem. By default, a UIButton doesn't need to do any custom drawing; it contains a nested instance of a private class, UIButtonLabel, that draws the button's title.
Instead of trying to override the drawing behavior of a class whose inner details are private, consider using one or more static images and simply setting the button's background image property for one or more states.
See if this changes anything...
-(void)setupView {
self.layer.cornerRadius = MIN(self.frame.size.height, self.frame.size.width) / 2.0;
self.clipsToBounds = YES;
}

calling setFrame multiple times?

i am calling setFrames multiple times for the same view.
for example after setting ltr views frames, i check if the layout should be rtl and change the views frames again.
-(void)setViewsFrames:(BOOL)RTL{
view1.frame = CGRECMAKE();
view2.frame = CGRECMAKE();
view3.frame = CGRECMAKE();
.
.
.
if(RTL){
[view1 setRtlFrame];
[view2 setRtlFrame];
[view3 setRtlFrame];
.
.
.
}
}
-(void)setRtlFrame{
CGRect RTLFrame = self.frame;
RTLFrame.origin.x = [self superview].frame.size.width - self.frame.origin.x - self.frame.size.width;
[self setFrame:RTLFrame];
}
does calling setFrames multiple time force the system to draw the view multiple times ? and may that effect performance.
I am using that also in UICollectionViewCell, so the system calls setViewsFrames: every time she want to draw the cell.
EDIT:
i've did a small test. i check when drawRect is called and here is the result:
it's called just one time, no matter how much times setFrame was called.
in UICollectionCellView it called just when the cell created or at reload.
setting the frame calls 'setNeedsLayout' and then on the next runloop iteration, IOS knows to layout & redraw the view.
there'd be no point in layouting / redrawing stuff the user doesn't see so iOS coalesces the calls for you -- if you let it by using the setNeedsXY methods
so the overhead of setting the frame is normally minimal
(except if you deal with custom (badly implemented) views [which you don't ;)])

How to implement sizeToFit if it depends on layoutSubviews?

In the documentation of layoutSubviews, Apple says:
You should not call this method directly.
I'm trying to implement sizeToFit. I want it to put a tight bounding box on all of the subviews. I have to layout the subviews before determining such a bounding box. That means I must call layoutSubviews, which Apple frowns upon. How would I solve this dilemma without violating Apple's rules?
- (void)layoutSubviews
{
[super layoutSubviews];
self.view0.frame = something;
self.view1.frame = somethingElse;
}
- (void)sizeToFit
{
[self layoutSubviews];
self.frame = CGRectMake(
self.frame.origin.x,
self.frame.origin.y,
MAX(
self.view0.frame.origin.x + self.view0.frame.size.width,
self.view1.frame.origin.x + self.view1.frame.size.width
),
MAX(
self.view0.frame.origin.y + self.view0.frame.size.height,
self.view1.frame.origin.y + self.view1.frame.size.height
)
);
}
One should not override -sizeToFit. Instead override -sizeThatFits: which is internally called by -sizeToFit with the view's current bounds size.
You should not override this method. If you want to change the default sizing information for your view, override the sizeThatFits: instead. That method performs any needed calculations and returns them to this method, which then makes the change. – UIView Class Reference
Also not that even if you would override -sizeToFit, there is most likely no reason to perform layout immediately. You only size the view, i.e. set its bounds size. This triggers a call to -setNeedsLayout, marking the view as needing layout. But unless you want to animate the view, the new layout does not have to be applied right away.
The point of this delayed update pattern is that it saves a lot of time if you perform multiple consecutive updates, since the actual update is only performed once.
I typically do this. It works like a charm.
#pragma mark - Layout & Sizing
- (void)layoutSubviews
{
[self calculateHeightForWidth:self.bounds.size.width applyLayout:YES];
}
- (CGSize)sizeThatFits:(CGSize)size
{
CGFloat const width = size.width;
CGFloat const height = [self calculateHeightForWidth:width applyLayout:NO];
return CGSizeMake(width, height);
}
- (CGFloat)calculateHeightForWidth:(CGFloat)width applyLayout:(BOOL)apply
{
CGRect const topViewFrame = ({
CGRect frame = CGRectZero;
...
frame;
});
CGRect const bottomViewFrame = ({
CGRect frame = CGRectZero;
...
frame;
});
if (apply) {
self.topView.frame = topViewFrame;
self.bottomView.frame = bottomViewFrame;
}
return CGRectGetMaxY(bottomViewFrame);
}
Note that the sample code is for a view that can be displayed at any width and the container would ask for the preferred height for a certain width.
One can easily adjust the code for other layout styles though.
I think it's very rare that you will need to use frame when using Auto Layout. Considering you want to solve the problem without breaking Apple rules, I would suggest using the Auto Layout way:
Setup constraints to determine the size of the container view.
Basically the constraints are setup in a way so that the container's width and height can be determined by the auto layout system.
For example, You can setup the constraints like:
Note that view0 and view1 must set both width and height constraints.
When you instantiate the view somewhere in your project, you don't need to setup the width and height constraint anymore. Auto layout will guess its size (called intrinsicContentSize) by the constraints you previously setup.
If you want to force layoutSubviews to happen, but don't want to call it directly, set the needsLayout flag, and then ask it to layout.
[self setNeedsLayout];
[self layoutIfNeeded];

Changing a frame in viewDidLayoutSubviews

When a view controller's view is first shown I want to run an animation in which all elements in the view controller slide from outside the bottom of the screen to their natural positions. To achieve this, I do subview.frame.origin.y += self.view.frame.size.height in viewDidLayoutSubviews. I also tried viewWillAppear, but it doesn't work at all. Then I animate them up to their natural positions with subview.frame.origin.y -= self.view.frame.size.height in viewDidAppear.
The problem is that viewDidLayoutSubviews is called several times throughout the view controller's lifespan. As such, when things like showing the keyboard happen all my content gets replaced outside the view again.
Is there a better method for doing this? Do I need to add some sort of flag to check whether the animation has already run?
EDIT: here's the code. Here I'm calling prepareAppearance in viewDidLayoutSubviews, which works, but viewDidLayoutSubviews is called multiple times throughout the controller's life span.
- (void)viewDidLayoutSubviews
{
[super viewDidLayoutSubviews];
[self prepareAppearance];
}
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
[self animateAppearance];
}
- (NSArray *)animatableViews
{
return #[self.createAccountButton, self.facebookButton, self.linkedInButton, self.loginButton];
}
- (void)prepareAppearance
{
NSArray * views = [self animatableViews];
NSUInteger count = [views count];
for (NSUInteger it=0 ; it < count ; ++it) {
UIView * view = [views objectAtIndex:it];
CGRect frame = view.frame;
// Move the views outside the screen, to the bottom
frame.origin.y += self.view.frame.size.height;
[view setFrame:frame];
}
}
- (void)animateAppearance
{
NSArray * views = [self animatableViews];
NSUInteger count = [views count];
for (NSUInteger it=0 ; it < count ; ++it) {
__weak UIView * weakView = [views objectAtIndex:it];
CGRect referenceFrame = self.view.frame;
[UIView animateWithDuration:0.4f
delay:0.05f * it
options:UIViewAnimationOptionCurveEaseOut
animations:^{
CGRect frame = weakView.frame;
frame.origin.y -= referenceFrame.size.height;
[weakView setFrame:frame];
}
completion:^(BOOL finished) {
}];
}
}
If you need to animate something when view will appear and then not touch subviews later, I would suggest the following:
Don't change/touch viewDidLayoutSubviews
Add logic to move elements outside the screen (to their initial position before animation) in viewWillAppear
Add logic to animate elements into their proper position in viewDidAppear
UPDATE:
If you're using auto-layout (which is very good thing), you can't animate views by changing their frames directly (because auto-layout would ignore that and change them again). What you need to do is to expose outlets to constraints responsible for Y-position (in your case) and change that constraints rather then setting frames.
Also don't forget to include call to [weakView layoutIfNeeded] after you update constraints in the animation method.
I did both things in viewDidAppear:. It seems that when viewDidAppear: is called, the view is not actually visible, but about to. So the UI elements never show in their initial position if they are replaced there.

Setting the maximum value of a UISlider causing strange behaviour

I am having a really weird issue trying to use a UISlider, when I change the maximumValue of the slider to > 1.0, I get this weird duplicate slider appearing on my view. Please help :)
Heres what it looks like when slider.maximumValue = 10.0;
http://imagizer.imageshack.us/v2/800x600q90/827/kf12.png
Heres what it looks like when slider.maximumValue = 1.0;
http://imagizer.imageshack.us/v2/800x600q90/197/bcvf.png
Is there some behaviour of a UISlider I am missing? Here is my code for configuring the slider...
-(void)layoutSubviews
{
[super layoutSubviews];
CGSize viewSize = self.contentSubview.bounds.size;
self.timeSlider.frame = CGRectMake(viewSize.width / 2 - 80,
viewSize.height / 2 - 12,
viewSize.width / 2,
24);
-(void)configureSubviews
{
[super configureSubviews];
self.timeSlider = [[UISlider alloc] initWithFrame:CGRectZero];
self.timeSlider.continuous = YES;
self.timeSlider.minimumValue = 0;
self.timeSlider.maximumValue = 1.0f;
[self.timeSlider addTarget:self action:#selector(timeValueChanged:) forControlEvents:UIControlEventValueChanged];
[self.contentSubview addSubview:self.timeSlider];
}
-(void)timeValueChanged:(UISlider *)slider
{
self.timeValueLabel.text = [NSString stringWithFormat:#"%d min", (int)floorf(slider.value)];
[self layoutSubviews];
}
layoutSubviews
Lays out subviews.
- (void)layoutSubviews
Discussion
The default implementation of this method does nothing on iOS 5.1 and earlier. Otherwise, the default implementation uses any constraints you have set to determine the size and position of any subviews.
Subclasses can override this method as needed to perform more precise layout of their subviews. You should override this method only if the autoresizing and constraint-based behaviors of the subviews do not offer the behavior you want. You can use your implementation to set the frame rectangles of your subviews directly.
You should not call this method directly. If you want to force a layout update, call the setNeedsLayout method instead to do so prior to the next drawing update. If you want to update the layout of your views immediately, call the layoutIfNeeded method.
Referance by apple doc

Resources