layoutSubviews vs frame didSet 检测视图的框架何时改变
Posted
技术标签:
【中文标题】layoutSubviews vs frame didSet 检测视图的框架何时改变【英文标题】:layoutSubviews vs frame didSet to detect when the view's frame changed 【发布时间】:2018-09-05 18:27:10 【问题描述】:假设我有一个 UIView 子类,当它的框架发生变化时需要更新一些东西。例如。当它在偶数像素上时,它会是红色的,当它在奇数像素上时会是蓝色的。
我已经看到了至少四种方法:
view.layoutSubviews()
:
override func layoutSubviews()
super.layoutSubviews()
backgroundColor = calculateColorFromFrame(frame)
view.frame
didSet
:
override var frame: CGRect
didSet
backgroundColor = calculateColorFromFrame(frame)
在视图的框架上添加一个 KVO 观察者。
在视图的包装 UIViewController 上,实现 viewDidLayoutSubviews()
。
override func viewDidLayoutSubviews()
super.viewDidLayoutSubviews()
view.updateBackgroundColorForNewFrame()
你知道的其他方式吗?
这些方法中的哪一种是首选,一种天生就比另一种更好?苹果官方的推荐是什么?
【问题讨论】:
【参考方案1】:layoutSubviews
:您可以依赖在大小已更改的视图上调用此函数,或者如果视图已收到 setNeedsLayout
。如果视图的位置发生变化但其大小没有变化,系统不会自动调用该视图上的layoutSubviews
。
frame didSet
:这适用于使用autoresizingMask
布局的视图。它不适用于使用普通约束布局的视图。相反,自动布局设置视图的center
和bounds
。然而,这些是在不同版本的 UIKit 中可能发生变化的实现细节。我在 Xcode 10 beta 6 附带的 ios 12.0 Simulator 上进行了测试。
frame
上的 KVO:由于与 #2 相同的原因,这在某些情况下不起作用。此外,frame
未记录为与 KVO 兼容,因此 UIKit 可以在不通知观察者的情况下更改 frame
。
viewDidLayoutSubviews
:只有当视图是视图控制器的view
属性时,这才有效,并且只有当view
收到layoutSubviews
消息时才有效(参见#1)。同样重要的是要了解,当调用它时,视图控制器的view
及其直接子视图已被布置,但更深的子视图尚未被布置。 (请参阅this answer 以获得更完整的解释。)
还要注意,视图的frame
是根据其layer
的一些属性计算得出的:层的position
、bounds.size
、anchorPoint
和transform
。如果有任何东西直接设置这些图层属性,则上述方法都不起作用。
由于所有这些,当视图的位置发生变化时,没有特别伟大的方法可以得到通知。技术上正确的方法可能是使用CAAction
。当该层检测到其position
正在更改时,它会向其委托(即视图本身,对于视图的层)询问要运行的操作。它通过发送actionForLayer:forKey:
消息来询问。因此,您可以在视图子类中覆盖 action(forLayer:forKey:)
以得到可靠的通知。但是,该层要求在更改其position
(因此更改其frame
)之前进行操作。它在更改其position
之后运行操作。所以你必须像这样跳一段小舞:
override func action(for layer: CALayer, forKey event: String) -> CAAction?
class MyAction: CAAction
init(view: MyView, nextAction: CAAction?)
self.view = view
self.nextAction = nextAction
let view: MyView
let nextAction: CAAction?
func run(forKey event: String, object: Any, arguments: [AnyHashable : Any]?)
// THIS IS WHERE YOU LOOK AT THE NEW FRAME AND ACT ACCORDINGLY.
print("This is the action. Frame now: \(view.frame)")
nextAction?.run(forKey: event, object: object, arguments: arguments)
let action = super.action(for: layer, forKey: event)
if event == "position"
return MyAction(view: self, nextAction: action)
else
return action
actionForLayer:forKey:
的 UIView
实现通常返回 nil
。但是在动画块内(包括当界面在纵向和横向之间旋转时),它返回(对于某些键)在图层上安装CAAnimation
的动作。因此,如果有 super
返回的操作,则运行该操作很重要。
【讨论】:
感谢您的详细回答。由于 frame 封装了位置和大小,我也很想知道如何正确处理大小变化。我会使用相同的 CAAction 技巧并寻找类似于position
的事件吗?如果是这样,我应该寻找哪个事件名称?我尝试在文档中查找值,我能找到的最好的是frame
或bounds
。但是,如果我使用其中任何一种,我似乎会遇到与您在第二个要点中提到的相同的问题。
如果你覆盖action(for:forKey:)
(就像在我的例子中)来打印密钥(event
),你会发现核心动画要求什么。我认为它从来没有要求"frame"
,只有"bounds"
和"position"
。
还要注意,如果视图的bounds.size
发生变化,视图将收到layoutSubviews
消息,因此您可以使用它来可靠地处理大小变化。【参考方案2】:
我可能对您的问题理解错误或误解了 Apple 文档,但以下是我对使用什么来查看框架更改的想法:
-
您不应直接调用 view.layoutSubviews()。 LayoutSubviews 方法可以被 UIView 的子类覆盖“仅当子视图的自动调整大小和基于约束的行为不提供您想要的行为时。您可以使用您的实现直接设置子视图的框架矩形”。 - 从 Apple 文档中复制。因此我不会使用这种方法,因为它不适用于您要更改颜色的视图,而是适用于您的视图的子视图。
我认为这是一种非常优雅的使用方式,因为如果您移动视图,框架将被设置为新值。但是,当框架改变大小时,didSet 也会被调用,并且框架可能不会移动。如果您的 calculateColorFromFrame(frame) 方法开销很大,并且视图可以更改大小,这可能会导致一些性能损失。
我认为这是实现与第二点相同的相当麻烦的方法。唯一的事情是您现在可以使用另一个类来观察视图框架的变化并调用 calculateColorFromFrame(frame) 方法。
viewDidLayoutSubviews() 在相应的视图控制器中也是一个好方法,但是当该视图控制器的主视图中的某些内容发生更改并且视图必须再次布置其子视图时,会调用此方法。这也意味着当您的视图没有移动或更改并且 calculateColorFromFrame(frame) 没有更改时,可以调用此方法。因此,这可能会再次多次调用此方法。
我很好奇的是你将如何有效地计算奇数和偶数像素,因为帧的位置和大小是基于点的,一个点可以跨越多个像素。
我希望这回答了你的问题。
【讨论】:
以上是关于layoutSubviews vs frame didSet 检测视图的框架何时改变的主要内容,如果未能解决你的问题,请参考以下文章
为啥 UITableViewCell.layoutSubviews() 的 frame.width 为 600(通用故事板宽度)而不是 320(特定 iPhone 设备)?