When should I be setting borders in UIViews? - ios

I've got a UIView that does not fill the whole screen and I would like to add a top border to that view. However, I keep getting the following:
Here is the code I am using:
CGFloat thickness = 4.0f;
CALayer *topBorder = [CALayer layer];
topBorder.frame = CGRectMake(0, 0, self.announcementCard.frame.size.width, thickness);
topBorder.backgroundColor = [UIColor blueColor].CGColor;
How I know why the border goes off the screen. This is because I put the border on the view inside the UIViews init method. When I do this the self.announcementCard.frame.size.width is 1000 and hence why the border goes off the screen. The self.announcementCard.frame.size.width has a width and height of 1000. The reason for this is because the UIView hasn't added the constraints to the UIView in its init methods.
Thus, my question is when should I be calling the code I've written above? When will self.announcementCard.frame.size.width have its constraints added to it and have its frame updated?

You should add your subviews (or sublayers) in the viewDidLoad method. However if you are using the auto-layout keep a reference of your sublayer and update it in the viewDidLayoutSubviews method:
- (void)viewDidLoad {
[super viewDidLoad];
_borderLayer = [CALayer layer];
[self.view.layer addSublayer:_borderLayer];
}
- (void)viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
_borderLayer.frame = CGRectMake(0, 0, self.view.bounds.size.width, 3);
}
Otherwise you can simply clipsToBounds the view to avoid the subviews to be visible beyond the bounds.
self.view.clipsToBounds = YES;

in the init the graphics isn't made yet. You have to put all your configuration on graphics object in the viewDidLoad: or viewWillAppear: of the UIViewController.

Short Answer:
viewWillAppear
By the time viewWillAppear is called, your subviews have been laid out and the frames are valid. Doing frame-based calculations in viewDidLoad can often have issues since the frames have not been set.

Related

When to layout subviews of UIViewControllers view?

In my UIViewController class, I'm creating a UIView called safeAreaView and adding it as a subview to the UIViewControllers view property. I'm making it so safeAreaView takes up the entire safe area of the UIViewControllers view property:
- (void) viewDidLoad
{
[self setToolbarWithColor: self.mainToolbarColor animated:NO];
self.tapGestureRecognizer.delegate = self;
self.view.clipsToBounds = YES;
self.view.backgroundColor = [UIColor lightGrayColor];
self.safeAreaView = [[UIView alloc] initWithFrame: CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height)];
self.safeAreaView.clipsToBounds = YES;
self.safeAreaView.delegate = self;
self.safeAreaView.translatesAutoresizingMaskIntoConstraints = NO;
[self.view addSubview: self.safeAreaView];
[self.safeAreaView.leadingAnchor constraintEqualToAnchor: self.view.safeAreaLayoutGuide.leadingAnchor].active = YES;
[self.safeAreaView.trailingAnchor constraintEqualToAnchor: self.view.safeAreaLayoutGuide.trailingAnchor].active = YES;
[self.safeAreaView.topAnchor constraintEqualToAnchor: self.view.safeAreaLayoutGuide.topAnchor].active = YES;
[self.safeAreaView.bottomAnchor constraintEqualToAnchor: self.view.safeAreaLayoutGuide.bottomAnchor].active = YES;
[self.safeAreaView loadSubviews];
}
This works fine, but my problem is, at some point after this during the UIViewControllers initialization cycle, safeAreaView updates to account for the statusbar (it's y position moves up 20 and it decreases in size by 20).
I need to layout some subviews on safeAreaView and I don't know the proper time? If I attach the subviews like above, they have the wrong height. And I can't use some auto layout features on the subviews because there are specific things that I need to do. I've also tried executing the above code in viewWillAppear with no luck.
Wondering if anyone had any suggestions?
You can override - (void)layoutSubviews on your safeAreaView class:
- (void)layoutSubviews {
[super layoutSubviews];
// Manual frame adjustment for non-autolayout participating subviews
// ...
}
Another option would be to override your safeAreaView's class frame setter, so each time the frame of your view changes, you'll get a chance to manually set any subview frames as needed.

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.

Creating an autolayout-based metrics view

I have a reusable view I will be using in UITableViewCell's and UICollectionViewCell's, and need to get its dimensions for tableView:heightForRowAtIndexPath:. Some subviews have stuff going on inside layoutSubviews so I can't call systemLayoutForContentSize:, instead my plan is to:
Instantiate the metrics view.
Set the size to include the desired width.
Populate it with data.
Update constraints / Layout subviews.
Grab the height of the view or an internal "sizing" view.
The problem I'm running into is that I cannot force the view to layout without inserting it into the view and waiting for the runloop.
I've distilled a rather boring example. Here's View.xib. The subview is misaligned to highlight that the view is never getting laid out even to the baseline position:
On the main thread I call:
UIView *view = [[UINib nibWithNibName:#"View" bundle:nil] instantiateWithOwner:nil options:nil][0];
NSLog(#"Subviews: %#", view.subviews);
view.frame = CGRectMake(0, 0, 200, 200);
[view updateConstraints];
[view layoutSubviews];
NSLog(#"Subviews: %#", view.subviews);
[self.view addSubview:view];
[view updateConstraints];
[view layoutSubviews];
NSLog(#"Subviews: %#", view.subviews);
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(#"Subviews: %#", view.subviews);
});
I get out the following view information:
1) "<UIView: 0x8bad9e0; frame = (50 50; 220 468); autoresize = W+H; layer = <CALayer: 0x8be0070>>"
2) "<UIView: 0x8bad9e0; frame = (50 50; 220 468); autoresize = W+H; layer = <CALayer: 0x8be0070>>"
3) "<UIView: 0x8bad9e0; frame = (50 50; 220 468); autoresize = W+H; layer = <CALayer: 0x8be0070>>"
4) "<UIView: 0x8bad9e0; frame = (0 100; 100 100); autoresize = W+H; layer = <CALayer: 0x8be0070>>"
1 indicates that the fresh-out-of-the-NIB view hasn't been laid out. 2 indicates that updateConstraints/layoutSubviews did nothing. 3 indicates that adding it to the view hierarchy did nothing. 4 finally indicates that adding to the view hierarchy and one pass through the main-loop laid out the view.
I would like to get to the point where I can get the view's dimensions without having to let the application handle it or perform manual calculations (string height + constraint1 + constraint2) on my own.
Update
I've observed that if I place view inside a UIWindow I get a slight improvement:
UIView *view = [[UINib nibWithNibName:#"View" bundle:nil] instantiateWithOwner:nil options:nil][0];
UIWindow *window = [[UIWindow alloc] initWithFrame:CGRectMake(0, 0, 200, 200)];
view.frame = CGRectMake(0, 0, 200, 200);
[window addSubview:view];
[view layoutSubviews];
If view.translatesAutoresizingMaskIntoConstraints == YES, the view's immediate subviews will be laid out, but none of their children.
The Autolayout Question
In the basic case you mentioned, you can get the correct size by calling setNeedsLayout and then layoutIfNeeded on the container view.
From the UIView class reference on layoutIfNeeded:
Use this method to force the layout of subviews before drawing. Starting with the receiver, this method traverses upward through the view hierarchy as long as superviews require layout. Then it lays out the entire tree beneath that ancestor. Therefore, calling this method can potentially force the layout of your entire view hierarchy. The UIView implementation of this calls the equivalent CALayer method and so has the same behavior as CALayer.
I don't think the "entire view hierarchy" applies to your use case since the metrics view presumably wouldn't have a superview.
Sample Code
In a sample empty project, with just this code, the correct frame is determined after layoutIfNeeded is called:
#import "ViewController.h"
#interface ViewController ()
#property (nonatomic, strong) UIView *redView;
#end
#implementation ViewController
#synthesize redView;
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
redView = [[UIView alloc] initWithFrame:CGRectMake(50, 50, 220, 468)];
redView.backgroundColor = [UIColor redColor];
redView.translatesAutoresizingMaskIntoConstraints = NO;
[self.view addSubview:redView];
NSLog(#"Red View frame: %#", NSStringFromCGRect(redView.frame));
// outputs "Red View frame: {{50, 50}, {220, 468}}"
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|[redView(==100)]" options:0 metrics:Nil views:NSDictionaryOfVariableBindings(redView)]];
[self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|-100-[redView(==100)]" options:0 metrics:Nil views:NSDictionaryOfVariableBindings(redView)]];
NSLog(#"Red View frame: %#", NSStringFromCGRect(redView.frame));
// outputs "Red View frame: {{50, 50}, {220, 468}}"
[self.view setNeedsLayout];
NSLog(#"Red View frame: %#", NSStringFromCGRect(redView.frame));
// outputs "Red View frame: {{50, 50}, {220, 468}}"
[self.view layoutIfNeeded];
NSLog(#"Red View frame: %#", NSStringFromCGRect(redView.frame));
// outputs "Red View frame: {{0, 100}, {100, 100}}"
}
#end
Additional Considerations
Slightly outside the scope of your question, here are some other issues you may run into, since I've worked on this exact problem in a real app:
Calculating this in heightForRowAtIndexPath: might be expensive, so you may want to precalculate and cache the results
Precalculation should be done on a background thread, but UIView layout doesn't work well unless it's done on the main thread
You should definitely implement estimatedHeightForRowAtIndexPath: to reduce the impact of these performance issues
Using intrinsicContentSize
In response to:
Some subviews have stuff going on inside layoutSubviews so I can't call systemLayoutForContentSize:
You can use this method if you implement intrinsicContentSize, which lets a view suggest an optimal size for itself. One implementation for this might be:
- (CGSize) intrinsicContentSize {
[self layoutSubviews];
return CGSizeMake(CGRectGetMaxX(self.bottomRightSubview.frame), CGRectGetMaxY(self.bottomRightSubview.frame));
}
This simple approach will only work if your layoutSubviews method doesn't refer to an already-set size (like self.bounds or self.frame). If it does, you may need to do something like:
- (CGSize) intrinsicContentSize {
self.frame = CGRectMake(0, 0, 10000, 10000);
while ([self viewIsWayTooLarge] == YES) {
self.frame = CGRectInset(self.frame, 100, 100);
[self layoutSubviews];
}
return CGSizeMake(CGRectGetMaxX(self.bottomRightSubview.frame), CGRectGetMaxY(self.bottomRightSubview.frame));
}
Obviously, you'd need to adjust these values to match the particular layout of each view, and you may need to tune for performance.
Finally, I'll add that due in part to the exponentially increasing cost of using auto-layout, for all but the simplest table cells, I usually wind up using manual height calculation.
Presumably you're calling the demo code when the view controller first loads its view, like in viewDidLoad or another life cycle method. The nested subview's geometries won't reflect its constraints until viewDidLayoutSubviews is called. Nothing you do during the initial life cycle of a view controller will make that method arrive any faster.
Update 12/30/13: After testing Aaron Brager's sample code, I now realize that the previous paragraph is incorrect. Apparently, you can force layout in viewDidLoad by calling setNeedsLayout followed by layoutIfNeeded.
If you executed the demo code in response to a button click instead, I think you'll see the final geometries of your nested subview logged before the action method completes.
- (IBAction)buttonTapped:(id)sender
{
UIView *view = [[UINib nibWithNibName:#"View" bundle:nil] instantiateWithOwner:nil options:nil][0];
view.frame = CGRectMake(0, 0, 200, 200);
[self.view addSubview:view];
[self.view layoutIfNeeded];
NSLog(#"Subviews: %#", view.subviews);
}
In the latter case, you can request layout on-demand because the view controller has completed its initial setup.
But during a view controller's initial setup, how are you going to get the final geometries of your re-usable subview?
After you set the content for the re-usable subview, have your view controller ask the subview for its size. In other words, implement a method on your custom view that calculates the size based on the content.
For example, if the subview's content is an attributed string, you could use a method like boundingRectWithSize:options:context: to help determine the size of your subview.
CGRect rect = [attributedString boundingRectWithSize:CGSizeMake(width, CGFLOAT_MAX) options:NSStringDrawingUsersLineFragmentOrigin context:nil];

Autolayout and shadow

I've got a problem with adding shadow to my UIView which is created in iOS 6 application with Autolayout.
Let's assume I have a method that adds a shadow on the bottom of UIView (this is actually a Category of UIView, so it's reusable):
- (void) addShadowOnBottom {
self.layer.shadowOffset = CGSizeMake(0, 2);
self.layer.shadowOpacity = 0.7;
self.layer.shadowColor = [[UIColor blackColor] CGColor];
self.layer.shadowPath = [UIBezierPath bezierPathWithRect:self.bounds].CGPath;
}
When I call this method in viewDidLoad of some UIViewController, shadow is not added, probably due to all constraints, that have to be calculated.
When I call this method in viewWillAppear the same situation.
When I call this method in viewDidAppear it works, but when new view shows up there is a short moment when there is no shadow and it appears after a while.
If I resign from setting the shadowPath and remove line self.layer.shadowPath everything works, but view transitions are not smooth.
So my question is what is the right way to add a shadow to view in iOS 6 with Autolayout turned on ?
Another thing you can add to the layer when working with AutoLayout and you need a shadow on a UIView where the frame is not yet known is this :
self.layer.rasterizationScale = [[UIScreen mainScreen] scale]; // to define retina or not
self.layer.shouldRasterize = YES;
Then remove the shadowPath property because the auto layout constraints are not yet processed, so it's irrelevant. Also at the time of execution you will not know the bounds or the frame of the view.
This improves performance a lot!
i removed self.layer.shadowPath = [UIBezierPath bezierPathWithRect:self.bounds].CGPath;
from your code and it is working for me in viewDidLoad, please confirm.
Increasing shdowOffset will make you see the shadow more clear.
Having the exact same issue...
Although I am unable to get the CALayer shadow on a view to animate nicely, at least the shadow does re-align properly after animation.
My solution (which works fine in my application) is the set the shadowOpacity to 0, then reset it to the desired value AFTER the animation has completed. From a user's perspective, you cannot even tell the shadow is gone because the animations are typically too fast to perceive the difference.
Here is an example of some code in my application, in which I am changing the constant value of a constraint, which is 'trailing edge to superview' NSLayoutContraint:
- (void) expandRightEdge
{
[self.mainNavRightEdge setConstant:newEdgeConstant];
[self updateCenterContainerShadow];
[UIView animateWithDuration:ANIMATION_DURATION delay:0 options:UIViewAnimationOptionCurveEaseInOut animations:^{
[[NSNotificationCenter defaultCenter] postNotificationName:#"PanelLayoutChanged" object:nil];
[self.view layoutIfNeeded];
} completion:^(BOOL finished) {
nil;
}];
}
- (void) updateCenterContainerShadow
{
self.centerContainer.layer.masksToBounds = NO;
self.centerContainer.layer.shadowOpacity = 0.8f;
self.centerContainer.layer.shadowRadius = 5.0f;
self.centerContainer.layer.shadowOffset = CGSizeMake(0, 0);
self.centerContainer.layer.shadowColor = [UIColor blackColor].CGColor;
CGPathRef shadowPath = [UIBezierPath bezierPathWithRect:self.centerContainer.layer.bounds].CGPath;
[self.centerContainer.layer setShadowPath:shadowPath];
// Schedule a time to fade the shadow back in until we can figure out the CALayer + Auto-Layout issue
[self performSelector:#selector(fadeInShadow) withObject:nil afterDelay:ANIMATION_DURATION+.05];
}
- (void) fadeInShadow
{
[self.centerContainer.layer setShadowOpacity:0.8f];
}
Two things:
I could have put the fadeInShadow in the completion block, but due to the way some of my other code is factored, this works better for me.
I realize I am not performing a fade in with "fadeInShadow", but given how quickly it renderes after the completion of the animation, I found it is not necessary.
Hope that helps!

iOS: Creating an overlay on top of an EAGLView / GLKView

I'm trying to create an overlay on top of a GLKView (effectively an EAGLView). I'm aware of the performance impact, but in my situation that's not a problem, since the scene is paused in the background, it merely needs to remain visible.
I've created a custom UIView called ReaderView whose only custom code is the following:
-(CALayer*)layer {
CATextLayer *textLayer = [[CATextLayer alloc] init];
// Layer settings.
[textLayer setCornerRadius:5.0f];
// Text settings.
[textLayer setFont:CGFontCreateWithFontName((CFStringRef)READING_FONT)];
[textLayer setFontSize:READING_FONT_SIZE];
[textLayer setAlignmentMode:kCAAlignmentJustified];
[textLayer setWrapped:YES];
return textLayer;
}
I've then called the following in a GLKViewController:
-(void)onMyCustomEvent {
if (_readerView==nil) {
CGRect frame = [[self view] frame];
frame.size.width *= 0.8f;
frame.size.height *= 0.8f;
_readerView=[[ReaderView alloc] initWithFrame:frame];
[_readerView setAutoresizingMask:UIViewAutoresizingFlexibleWidth|UIViewAutoresizingFlexibleHeight];
}
[_readerView setText:[node content]];
[[self view] addSubview:_readerView];
}
NSLog has proven this method gets called and the reader view gets initialized. However nothing displays on top of the GLKView.
Any idea why this doesn't work?
Since you're adding it as a subview, you should use the bounds property instead of the frame property from the parent view, like this:
CGRect frame = self.view.bounds;
If that doesn't fix it, try setting the background color of the _readerView to something noticable to ensure that the problem isn't the content missing, like this:
_readerView.backgroundColor = [UIColor redColor];
Finally, if none of that solves it, then maybe directly adding subviews to an EAGL view is the problem. In that case, create a container view and add your EAGL view and your _readerView to that instead.

Resources