如何使用 Xcode 8 将 UIView 作为具有圆角半径的圆

Posted

技术标签:

【中文标题】如何使用 Xcode 8 将 UIView 作为具有圆角半径的圆【英文标题】:How to have UIView as a circle with corner radius with Xcode 8 【发布时间】:2016-09-26 12:31:20 【问题描述】:

我在设置视图的角半径时遇到问题,因为自从 Xcode 更新为 8 后,视图的帧在大多数情况下都设置为 1000x1000 而不是适当的大小。

有一个类似的问题here,但我想添加更多信息,希望有人找到答案或解决方法。

在其中一个实例中,我有一个表格视图单元格,其中有一个在情节提要中创建的图像视图。它的宽度和高度设置为具有约束的常数(至 95)。圆角半径设置在layoutSubviews 中,目前已经生效:

- (void)layoutSubviews

    [super layoutSubviews];
    self.myImageView.layer.cornerRadius = self.myImageView.frame.size.width/2.0f;

所以我尝试了一些尝试和错误,如果我尝试在设置图像(从远程服务器检索图像)后通过调用触发布局,情况似乎会更好:

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

这在大多数情况下仍然不起作用。现在有趣的部分:

我在标签栏上有一个视图控制器的例子,当我用目标视图控制器按下标签时,将立即调用布局子视图(来自UIApplicationMain),其中图像视图的大小为 1000x1000但是一旦图像被设置并且我调用布局方法然后布局子视图被再次调用,正确的图像视图大小为 95x95 得到正确的结果。

第二种情况是推送相同类型的视图控制器,现在布局首先没有被调用,唯一的调用是在设置图像并强制布局之后进行的。结果又是一个大小为 1000x1000 的图像视图,将角半径设置为 500,图像不可见。

那么有没有一个很好的解决方案来修复这些奇怪的帧值?我在多个视图控制器、表格视图单元格上遇到了同样的问题。在更新之前,这一切都可以正常工作。

有没有人知道这个问题的根源是什么?是来自情节提要还是某种设置迁移,还是这个版本的软件引入了一些布局错误?

我现在还添加了从笔尖唤醒:

- (void)awakeFromNib

    [super awakeFromNib];

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

在我的情况下,现在强制布局至少发生 2 次,并且第二次帧是正确的。但这远不是答案,这太糟糕了,我想爱它,因为我需要在每个图像视图、按钮的所有单元格上使用它。

【问题讨论】:

我在第一次显示单元格时从未以正确的帧大小调用表格单元格上的覆盖 layoutSubviews 时遇到类似的问题。我怀疑这是某种布局错误,因为布局系统发生了相当大的变化。 @NewShelbyWoo 我为此问题添加了解决方法的答案。请让我知道这是否能暂时解决您的问题。谢谢。 我最终通过调用myImageView.layoutIfNeeded 修复它,然后在layoutSubviews 中对其进行操作 layoutSubviews 方法在什么类上? @robmayoff 这篇文章现在应该被弃用,因为苹果已经修复了这个奇怪的行为。 layoutSubviews 是直接在 UIView 上的方法,通常不应调用,但可用于覆盖。您遇到了什么问题? 【参考方案1】:

你能把你的方法更新为

- (void)layoutSubviews

    [super layoutSubviews];
    self.myImageView.layer.cornerRadius = self.myImageView.frame.size.width/2.0f;
    self.myImageView.clipsToBounds = TRUE;

试一试。

因为在我的一个案例中,我错过了设置 clipsToBounds = TRUE; 并且它没有让它循环。

【讨论】:

剪辑子视图已启用并且可以正常工作。是的,在图像视图上,您​​需要启用它或根本不进行剪辑。但这不是我的情况,在我的情况下,视图正确显示或根本不显示。而且我确实知道为什么不这样做,因为拐角半径太大。而它之所以这么大,是因为image view的frame太大,原因完全不知道。 好吧,我错过了。但我遇到了同样的问题并使用 clipsToBounds 解决了,所以共享答案。【参考方案2】:

我猜问题中显示的 layoutSubviews 覆盖位于 UITableViewCell 的自定义子类上。

这里的问题可能是myImageView不是单元格的直接子视图。

了解单元格对[super layoutSubviews] 的调用仅设置单元格的直接 子视图的框架很重要。它不会在返回之前一直沿着视图层次结构向下走。单元格通常只有一个直接子视图:contentView。所以对[super layoutSubviews] 的调用只是设置了内容视图的框架,然后返回。设置self.myImageView.layer.cornerRadius 的尝试发生得太快了,因为self.myImageView.frame 尚未更新。

稍后,单元格的layoutSubviews方法返回后,UIKit会发送layoutSubviews消息给内容视图,此时内容视图会设置自己的直接子视图的帧。那时myImageView的框架将被设置。

最简单的解决方法是创建一个UIImageView 子类并覆盖其layoutSubviews 以设置其自己的圆角半径。

@interface CircleImageView: UIImageView
@end

@implementation CircleImageView

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


@end

更新

Matic Oblak 问道(在评论中),“这是你在这里写的可测试的吗”?

是的,它是可测试的。这是一种测试方法:创建一个视图层次结构,其中包含根视图、根视图内的容器视图和容器视图内的叶视图:

在根视图和容器视图中覆盖 layoutSubviews 以打印视图的框架:

// 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

为了更好地衡量,让我们也看看视图控制器的 viewWillLayoutSubviewsviewDidLayoutSubviews 何时运行:

// ViewController.m

@implementation ViewController

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


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


@end

最后,在与故事板中大小不同的设备或模拟上运行它,然后查看调试控制台:

-[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

注意根视图的[super layoutSubviews] 改变了容器视图的框架,但改变了叶视图的框架。然后,容器视图的[super layoutSubviews] 改变了叶子视图的大小。

因此,当layoutSubviews 在视图中运行时,您可以假设该视图的直接子视图的框架已更新,但您必须假设任何嵌套更深的视图的框架没有 已更新。

还要注意viewDidLayoutSubviews 在视图控制器的视图完成layoutSubviews 之后运行,但 更深嵌套的后代运行layoutSubviews 之前。所以在viewDidLayoutSubviews,你必须再次假设嵌套更深的视图的框架没有更新。

有一种方法可以强制立即更新嵌套更深的子视图的框架:将layoutIfNeeded 发送到其父视图。比如我可以修改-[RootView layoutSubviews],将layoutIfNeeded发送到_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));

(请注意,在此示例项目中,_leafView.superview_containerView,但一般情况下可能不是。)结果如下:

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

所以现在我强制 UIKit 在 -[RootView layoutSubviews] 返回之前更新 _leafView 的框架。

至于“我假设在标签上设置简单文本等许多操作都会调用 setNeedsLayout”:您可以使用我刚刚演示的相同技术来找出该问题的答案。

【讨论】:

您在此处写的内容是否可测试,或者您有任何相关文档吗?我不确定是谁触发了超级视图上的布局标志,但我希望没有。只要它的任何子视图需要布局,视图就需要布局。标志在那里,因此布局不会出现太多次。我假设许多操作(例如在标签上设置简单文本)将调用 setNeedsLayout ,此时它要么对其父视图执行此操作,要么 getter 标志检查所有子视图以查看是否需要布局...在这两种情况下我都希望布局将在两个视图的同一个运行循环中调用... ...但是在您所描述的内容中,您仅在其上设置了文本的标签实际上会布局,这意味着例如位于它旁边的图标不会移动(它应该并且确实会移动)在自动布局中)。所以按照这个逻辑,我很难相信你写的东西是真的。我错过了什么吗?【参考方案3】:

尝试重写 UIImageView 子类中的 drawRect: 方法,然后设置圆角半径。

- (void)drawRect:(CGRect)rect 
self.layer.cornerRadius = self.frame.size.width/2.0f;
[self setClipsToBounds:YES];

【讨论】:

【参考方案4】:

我创建了一个演示应用程序来查看发生此问题的最低要求是什么,而且要求非常低:

创建一个新的单视图应用程序 在主视图控制器上添加表格视图 向表格视图添加一个单元格并将其类覆盖为新的表格视图单元格子类 将图像视图添加到具有固定宽度和高度约束的单元格(同时添加前导和顶部约束) 在单元格layoutSubviews 中,将图像视图角半径设置为图像视图宽度的一半(结果将始终为 500)

由于圆角半径为500,图像视图不显示。

所以我正在寻找一个最小的修复程序,从之前的测试来看,布局似乎缺少第一次调用,所以我通过覆盖 nib 的 awake 来添加它:

- (void)awakeFromNib

    [super awakeFromNib];
    [self setNeedsLayout];
    [self layoutIfNeeded];

因此,在我目前正在处理的项目中,我创建了一个表格视图单元格的新子类,并且我已经覆盖了从 nib 唤醒。然后我将所有其他单元格作为这个单元格的子类。这不是一个解决方案,但它是一种解决方法,因为我有 57 个表格视图单元子类,所以这个过程实施起来非常快。

【讨论】:

【参考方案5】:

只需为圆形视图添加@pushkraj 答案,您的 UIView 宽度和高度应该相同。

那你就可以了。

- (void)layoutSubviews

   [super layoutSubviews];

   view.layer.cornerRadius = view.frame.size.height/2
   view.clipsToBounds = true

【讨论】:

【参考方案6】:

你可以这样做

- (void)layoutSubviews

    [super layoutSubviews];
    self.myImageView.layer.cornerRadius = self.myImageView.frame.size.width/2.0f;
    self. myImageView.layer.masksToBounds = YES;

这对你有用。 了解为什么需要将图层屏蔽到边界 link here

【讨论】:

这看起来像是来自使用剪辑子视图属性的@Pushkraj 的重复答案。你有什么理由可以解释为什么这更好吗?完成此操作后,您能否解释一下这是如何解决拐角半径正在工作并且启用了裁剪的事实,但问题在于视图大小为 1000x1000,而不管应使其成为 95x95 的约束如何。【参考方案7】:

要使任何视图为圆形,您必须更新圆角半径,圆角半径是任何视图的layer 的属性(viewInfo)。

来自苹果文档

将半径设置为大于 0.0 的值会导致图层开始在其背景上绘制圆角。默认情况下,圆角半径不适用于图层内容属性中的图像;它仅适用于图层的背景颜色和边框。但是,将 maskToBounds 属性设置为 true 会导致内容被裁剪为圆角。

此属性的默认值为 0.0。

More about corner radius

这里是与swift 4兼容的代码sn-p

@IBOutlet weak var viewInfo: UIView!

func setCornerRadius()
    self.viewInfo.layer.cornerRadius = self.viewInfo.frame.size.height/2
 

【讨论】:

以上是关于如何使用 Xcode 8 将 UIView 作为具有圆角半径的圆的主要内容,如果未能解决你的问题,请参考以下文章

在滚动视图中更改 UIView 而 / 更新滚动视图高度 - Swift 3 / Xcode 8

iOS:UIView 的 Autolayout 拉伸高度过多(Xcode 8、Swift 3)

Xcode 8 Storyboard 帧大小问题

如何将事件发送到我的(子)UIView?

如何在 XCODE 中将 UIView 水平分成三个大小均匀的子视图?

xCode UIView 与 UITableView [关闭]