How to implement sizeToFit if it depends on layoutSubviews? - ios

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];

Related

CALayer shadow in UITableViewCell Drawn incorrectly

I am applying shadow to a UITableViewCell using CALayer.
Here's my code:
- (void)addShadowToView:(UIView *)view
{
// shadow
view.layer.shadowColor = [[UIColor colorWithWhite:0.0f alpha:0.1f] CGColor];
view.layer.shadowOpacity = 1.0f;
view.layer.shadowOffset = CGSizeMake(0.0f, 3.0f);
view.layer.shadowRadius = 6.0f;
CGRect shadowFrame = view.layer.bounds;
CGPathRef shadowPath = [UIBezierPath bezierPathWithRect:shadowFrame].CGPath;
view.layer.shadowPath = shadowPath;
}
The issue is that for some tableviewcells, the shadow does not span the entire width of the cell. For some cells it would be correct, for others it would be faulty. I do notice that the rotation of the device also affects it, and reloading of the tableview data sometimes solves it.
What is the best way to mitigate this issue (and with that I don't mean to reload the whole tableview on each rotation etc.)?
Example bottom of cell where shadow is correctly applied:
Bottom of cell in same tableview after scrolling down (shadow only applied for first 75% of width):
Edit: I have noticed the issue is caused from these lines of code:
CGRect shadowFrame = view.layer.bounds;
CGPathRef shadowPath = [UIBezierPath bezierPathWithRect:shadowFrame].CGPath;
view.layer.shadowPath = shadowPath;
If I leave them out, everything is fine. But I've been told there is certain performance benefit when using this. Somehow the shadow is not correctly applied to new dimensions after rotating..
You can override the setter for you're cell's frame and call addShadowToView:. You can optimize this more by storing your cell's size and only updating the shadow path when the size changes for example:
#property (nonatomic, assign) CGSize size;
And
- (void) setFrame:(CGRect)frame
{
[super setFrame:frame];
// Need to check make sure this subview has been initialized
if(self.subviewThatNeedsShadow != nil && !CGSizeEqualToSize(self.size,_frame.size)
{
[self addShadowToView: self.subviewThatNeedsShadow];
}
}
The easiest solution is to add the shadow to the UITableViewCell's contentView (vs the layer for the cell's backing view). Since the cell's bounds change on scroll, if you add the shadow to the root view then you would have to update the shadow's path on each scroll event which would be costly and not necessary.
You're definitely correct re: the performance hit by not explicitly setting the shadowPath though. If you don't have any animated content within the cell, I'd also recommend rasterizing it to further improve performance.
EDIT: You must also ensure that when you set the shadow path that the contentView's bounds are in their 'final' position. If the size of the cell is later modified, this will result in the contentView's bounds changing and thus an incorrect shadowPath. The solution to this is to update the path in the UITableViewCell's -layoutSubviews method.
Here the concern is not the parent view frame where your working here concern is its sublayer and its size which should be changed when layout changes. You can override the below method which will help you to setup correct frame on layout changing.
public override void LayoutSublayersOfLayer(CALayer layer)
{
base.LayoutSublayersOfLayer(layer);
if (layer.Name == "gradient")
{
layer.Frame = view.Layer.Frame;
}
}
In above code view is the where you added sublayer. If you are playing with multiple layers in same view than you can use the identifier name property to work on particular layer.
Thanks for #beyowulf's answer gave me clues in override UIView frame get and set
In my case, I would like to make shadow stick with the other subview in subclass tableView cell.
Swift 5
// TargetView old size
var lastSize: CGSize = .zero
// Override frame in subclass tableView cell
override var frame: CGRect {
get {
super.frame
}
set {
super.frame = newValue
if targetView != nil {
// Compared targetView size with old one.
if lastSize != targetView.frame.size {
/* Update the other subview's shadow path or layer frame here */
lastSize = targetView.frame.size
}
}
}
}
It works for me.

Deciding height for UIScrollView with UIWebView inside

I have a detail view where I want to show a title, subtitle and content for articles. I want to be able to use HTML to format the text, so I've used a UIWebView for showing the article body. This works perfectly.
How ever, all of this, is inside a UIScrollView, so my issue is that I have to calculate the height of the UIScrollView?
This is how it works today:
And this is how it looks like in Storyboard:
So what I need to find out, is what is the correct code and syntax to calculate the correct height of the UIScrollView? Amongst several things, I tried [self.scrollView sizeToFit] without luck.
EDIT: Apparently it sets the correct heights with the code below, but seems like the view never updates.
-(void)webViewDidFinishLoad:(UIWebView *)webView
{
// get height of content in webview
CGFloat height = [[webView stringByEvaluatingJavaScriptFromString:#"document.body.scrollHeight;"] floatValue];
// set new frame height
CGRect frame = webView.frame;
frame.size.height = height;
webView.frame = frame; // webview is now correct height
// set new frame height for scrollview (parent of webview)
CGRect scrollFrame = self.scrollView.frame;
scrollFrame.size.height = webView.frame.origin.y + height;
self.scrollView.frame = scrollFrame;
// log to console for cross checking
NSLog(#"new frame: %f, scrollview frame: %f", scrollFrame.size.height, self.scrollView.frame.size.height);
}
The console reports the apparently correct height:
new frame: 582.000000, scrollview frame: 582.000000
And a quick check in Photoshop as well, this seems to be correct:
The summed value of green and blue area is 582 pixels, but the scrollview still just scrolls the 504 pixel area from below the navigation bar to the bottom of the screen (to the bottom of the tab bar).
The webview has internally a scrollview. You can query its size by webview.scrollView.contentSize. You have to wait with this until the webview has finished rendering.
So, in the -webViewDidFinishLoad: delegate method you can get the optimal height of the webView through webView.scrollView.contentSize.height. You can then resize the webView to this height and layout the other views appropriately. If all of this is done in a custom view, the proper way of doing this would probably be to just call [theView setNeedsLayout] and override -layoutSubviews in theView.
You also should set webView.scrollView.alwaysBounceVertically to NO.
I solved the problem.
First of all, just expand the UIWebView to a height higher than the content ever will be (e.g. 2000 pixels).
The delegate method code that makes the magic happen
-(void)webViewDidFinishLoad:(UIWebView *)webView
{
// set height for webiew
webView.autoresizesSubviews = NO;
webView.translatesAutoresizingMaskIntoConstraints = YES;
webView.scrollView.autoresizesSubviews = NO;
webView.scrollView.translatesAutoresizingMaskIntoConstraints = YES;
CGFloat height = [[webView stringByEvaluatingJavaScriptFromString:#"document.getElementById('content').clientHeight;"] floatValue] + 80; // +80 for tabbar and spacing
CGRect frame = webView.frame;
frame.size.height = height;
webView.frame = frame;
// fix height of scroll view as well
self.scrollView.translatesAutoresizingMaskIntoConstraints = YES;
self.scrollView.contentSize = CGSizeMake(320, (self.webView.frame.origin.y + self.webView.frame.size.height));
}

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

iOS: determine when all children have had layoutSubviews called

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++;
}
}

How to do a custom layout of a UIView so it works with both iPhone 5 screen dimensions as well as regular screen dimensions

Here is the desired outcome. The blue area is the UIView of interest. The UIView is not a UIImageView.
I've tried all sorts of arrangements with auto-resizing masks to no avail
This can only be done programmatically. One option is what #user2223761 suggests with subclassing. If you don't want to subclass UIView, then you need to set the frames on orientation changes and set yourView.center to be the center of the center.
- (void) willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation duration:(NSTimeInterval)duration {
if (UIInterfaceOrientationIsLandscape(interfaceOrientation)) {
// Make sure that the frame is centered in the screen
NSInteger paddingLeftSide = (self.view.bounds.size.width - 480) / 2;
self.view.frame = CGRectMake(paddingLeftSide, 0, 480, 320);
} else {
self.view.frame = CGRectMake(0, 0, 320, 320);
}
}
Dealing with different screen sizes can be tricky. In your case it is not :)
since you want to center the view in the screen what ever size it is, all you need to do is set the center of the view to be the center of the screen.
CGRect screenBounds = [[UIScreen mainScreen] bounds];
view.center = CGPointMake(screenBounds.size.width/2,screenBounds.size.height/2);
This code assumes the view's superView's bounds is the same size as the screenBounds..
First: Subclass UIView (create a MYUIView).
Second: override the method
- (void)layoutSubviews {
[super layoutSubviews];
// .. put your code...
}
and perform the frame update manually inside that method by reading the screen size.
auto-resize mask must be set to UIViewAutoresizingNone.

Resources