Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to have UIView as a circle with corner radius with Xcode 8

I am having a problem with setting the corner radius on the view because ever since the Xcode update to 8 the frames of the views are in most cases set to 1000x1000 instead of appropriate size.

There is a similar question here but I would like to add a bit more info in hope someone found the answer or a workaround.

In one of the instances I have a table view cell which has an image view created in storyboard. Its width and height are set as constant with constraint (to 95). The corner radius is set in the layoutSubviews which has been working for now:

- (void)layoutSubviews
{
    [super layoutSubviews];
    self.myImageView.layer.cornerRadius = self.myImageView.frame.size.width/2.0f;
}

So I played around a bit with trial and error and the situation seems to perform a bit better if I try to trigger layout after the image is set (an image is retrieved from remote server) by calling:

self.myImageView.image = image;
[self.myImageView setNeedsLayout];
[self setNeedsLayout];
[self layoutIfNeeded];

Still this does not work in most cases. Now the funny part:

I have a case of this view controller on the tab bar where when I press the tab with the target view controller there will be a call to layout subviews immediately (from UIApplicationMain) where the size of the image view is 1000x1000 but once the image is set and I call the layout methods then the layout subviews is called again with correct image view size of 95x95 getting a correct result.

The second situation is pushing the view controller of the same type where now the layout is not being called at first, the only call is made after the image is set and layout is forced. The result is again having an image view of size 1000x1000, setting the corner radius to 500 and the image is not visible.

So is there a nice solution to fix these strange frame values? I have the same problem on multiple view controllers, table view cells. It all used to work until the update.

Has anyone any ideas what is the source of this problem even? Is it from the storyboard or some sort of settings migration or is this some layout bug introduced with this version of software?

I now also added the awake from nib:

- (void)awakeFromNib
{
    [super awakeFromNib];

    [self.myImageView setNeedsLayout];
    [self setNeedsLayout];
    [self layoutIfNeeded];
}

Which now forces the layout to happen at least 2 times in my case and the second time the frames are correct. Still this is far from the answer, this is awful and I would like to avid it since I would need to use it on all of the cells for each of the image views, buttons.

like image 480
Matic Oblak Avatar asked Sep 26 '16 12:09

Matic Oblak


2 Answers

I guess the layoutSubviews override shown in the question is on a custom subclass of UITableViewCell.

The problem here is probably that myImageView is not a direct subview of the cell.

It's important to understand that the cell's call to [super layoutSubviews] only sets the frames of the cell's direct subviews. It doesn't walk all the way down the view hierarchy before returning. The cell normally has only one direct subview: its contentView. So the call to [super layoutSubviews] just sets the frame of the content view and then returns. The attempt to set self.myImageView.layer.cornerRadius is happening too soon, because self.myImageView.frame hasn't been updated yet.

Later, after the cell's layoutSubviews method returns, UIKit will send the layoutSubviews message to the content view, at which time the content view will set its own direct subview's frames. That is when myImageView's frame will be set.

The easiest fix for this is just to make a UIImageView subclass and override its layoutSubviews to set its own corner radius.

@interface CircleImageView: UIImageView
@end

@implementation CircleImageView

- (void)layoutSubviews {
    [super layoutSubviews];
    self.layer.cornerRadius = self.bounds.size.width / 2;
}

@end

UPDATE

Matic Oblak asks (in a comment), “Is this what you wrote here testable”?

Yes, it's testable. Here's one way to test it: create a view hierarchy with a root view, a container view inside the root view, and a leaf view inside the container view:

view hierarchy

Override layoutSubviews in the root view and the container view to print the frames of the views:

// RootView.m

@implementation RootView {
    IBOutlet UIView *_containerView;
    IBOutlet UIView *_leafView;
}

- (void)layoutSubviews {
    NSLog(@"%s before super: self.frame=%@ _containerView.frame=%@ _leafView.frame=%@", __FUNCTION__, NSStringFromCGRect(self.frame), NSStringFromCGRect(_containerView.frame), NSStringFromCGRect(_leafView.frame));
    [super layoutSubviews];
    NSLog(@"%s after super: self.frame=%@ _containerView.frame=%@ _leafView.frame=%@", __FUNCTION__, NSStringFromCGRect(self.frame), NSStringFromCGRect(_containerView.frame), NSStringFromCGRect(_leafView.frame));
}

@end

// ContainerView.m

@implementation ContainerView {
    IBOutlet UIView *_leafView;
}

- (void)layoutSubviews {
    NSLog(@"%s before super: self.frame=%@ _leafView.frame=%@", __FUNCTION__, NSStringFromCGRect(self.frame), NSStringFromCGRect(_leafView.frame));
    [super layoutSubviews];
    NSLog(@"%s after super: self.frame=%@ _leafView.frame=%@", __FUNCTION__, NSStringFromCGRect(self.frame), NSStringFromCGRect(_leafView.frame));
}

@end

For good measure, let's also see when the view controller's viewWillLayoutSubviews and viewDidLayoutSubviews are run:

// ViewController.m

@implementation ViewController

- (void)viewWillLayoutSubviews {
    [super viewWillLayoutSubviews];
    NSLog(@"%s", __FUNCTION__);
}

- (void)viewDidLayoutSubviews {
    [super viewDidLayoutSubviews];
    NSLog(@"%s", __FUNCTION__);
}

@end

Finally, run it on a device or simulation at a different size than in the storyboard, and look at the debug console:

-[ViewController viewWillLayoutSubviews]
-[RootView layoutSubviews] before super: self.frame={{0, 0}, {375, 667}} _containerView.frame={{20, 40}, {280, 420}} _leafView.frame={{20, 20}, {240, 380}}
-[RootView layoutSubviews] after super: self.frame={{0, 0}, {375, 667}} _containerView.frame={{20, 40}, {335, 607}} _leafView.frame={{20, 20}, {240, 380}}
-[ViewController viewDidLayoutSubviews]
-[ContainerView layoutSubviews] before super: self.frame={{20, 40}, {335, 607}} _leafView.frame={{20, 20}, {240, 380}}
-[ContainerView layoutSubviews] after super: self.frame={{20, 40}, {335, 607}} _leafView.frame={{20, 20}, {295, 567}}

Notice that the root view's [super layoutSubviews] changes the frame of the container view, but does not change the frame of the leaf view. Then, the container view's [super layoutSubviews] changes the size of the leaf view.

Therefore, when layoutSubviews has run in a view, you can assume that the frames of the view's direct subviews have been updated, but you must assume that the frames of any more deeply-nested views have not been updated.

Note also that viewDidLayoutSubviews runs after the view controller's view has done layoutSubviews, but before the more deeply-nested descendants have run layoutSubviews. So in viewDidLayoutSubviews, you must again assume that the frames of the more deeply-nested views have not been updated.

There is a way to force the frame of a more deeply-nested subview to be updated immediately: send layoutIfNeeded to its superview. For example, I can modify -[RootView layoutSubviews] to send layoutIfNeeded to _leafView.superview:

- (void)layoutSubviews {
    NSLog(@"%s before super: self.frame=%@ _containerView.frame=%@ _leafView.frame=%@", __FUNCTION__, NSStringFromCGRect(self.frame), NSStringFromCGRect(_containerView.frame), NSStringFromCGRect(_leafView.frame));
    [super layoutSubviews];
    NSLog(@"%s after super: self.frame=%@ _containerView.frame=%@ _leafView.frame=%@", __FUNCTION__, NSStringFromCGRect(self.frame), NSStringFromCGRect(_containerView.frame), NSStringFromCGRect(_leafView.frame));
    [_leafView.superview layoutIfNeeded];
    NSLog(@"%s after _leafView.superview: self.frame=%@ _containerView.frame=%@ _leafView.frame=%@", __FUNCTION__, NSStringFromCGRect(self.frame), NSStringFromCGRect(_containerView.frame), NSStringFromCGRect(_leafView.frame));
}

(Note that in this example project, _leafView.superview is _containerView, but in general it might not be.) Here's the result:

-[ViewController viewWillLayoutSubviews]
-[RootView layoutSubviews] before super: self.frame={{0, 0}, {375, 667}} _containerView.frame={{20, 40}, {280, 420}} _leafView.frame={{20, 20}, {240, 380}}
-[RootView layoutSubviews] after super: self.frame={{0, 0}, {375, 667}} _containerView.frame={{20, 40}, {335, 607}} _leafView.frame={{20, 20}, {240, 380}}
-[ContainerView layoutSubviews] before super: self.frame={{20, 40}, {335, 607}} _leafView.frame={{20, 20}, {240, 380}}
-[ContainerView layoutSubviews] after super: self.frame={{20, 40}, {335, 607}} _leafView.frame={{20, 20}, {295, 567}}
-[RootView layoutSubviews] after _leafView.superview: self.frame={{0, 0}, {375, 667}} _containerView.frame={{20, 40}, {335, 607}} _leafView.frame={{20, 20}, {295, 567}}
-[ViewController viewDidLayoutSubviews]

So now I have forced UIKit to update the frame of _leafView before -[RootView layoutSubviews] returns.

As for “I assume many operations like setting a simple text on the label will call setNeedsLayout”: you can use the same technique I just demonstrated to figure out the answer to that question.

like image 86
rob mayoff Avatar answered Oct 18 '22 13:10

rob mayoff


Try to override drawRect: method in UIImageView subclass and then set the corner radius.

- (void)drawRect:(CGRect)rect {
self.layer.cornerRadius = self.frame.size.width/2.0f;
[self setClipsToBounds:YES];
}
like image 36
SThakur Avatar answered Oct 18 '22 12:10

SThakur