首页 > 解决方案 > layoutSubviews vs frame didSet 检测视图的框架何时改变

问题描述

假设我有一个 UIView 子类,当它的框架发生变化时需要更新一些东西。例如,当它在偶数像素上时,它会是红色的,当它在奇数像素上时会是蓝色的。

我已经看到至少四种方法可以做到这一点:

  1. view.layoutSubviews()

    override func layoutSubviews() {
        super.layoutSubviews()
        backgroundColor = calculateColorFromFrame(frame)
    }
    
  2. view.frame didSet

    override var frame: CGRect {
        didSet {
            backgroundColor = calculateColorFromFrame(frame)
        }
    }
    
  3. 在视图的框架上添加一个 KVO 观察者。

  4. 在视图的包装 UIViewController 上,实现viewDidLayoutSubviews().

    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        view.updateBackgroundColorForNewFrame()
    }
    
  5. 你知道的还有其他方法吗?

这些方法中的哪一种是首选的,并且一种本质上比另一种更好?苹果官方的推荐是什么?

标签: iosswiftuiview

解决方案


  1. layoutSubviews:您可以依靠在大小已更改的视图上调用此方法,或者如果视图已收到setNeedsLayout. 如果视图的位置改变但它的大小没有改变,系统不会自动调用layoutSubviews那个视图。

  2. frame didSet:这适用于使用autoresizingMask. 它不适用于使用普通约束布局的视图。相反,自动布局设置视图的centerbounds. 然而,这些是在不同版本的 UIKit 中可能发生变化的实现细节。我在 Xcode 10 beta 6 附带的 iOS 12.0 模拟器上进行了测试。

  3. KVO on frame:这在某些情况下不起作用,原因与 #2 相同。此外,没有记录到与 KVO 兼容,因此 UIKit 可以在不通知观察者的情况下frame进行更改。frame

  4. viewDidLayoutSubviews:只有当视图是view视图控制器的属性时,这才有效,并且只有当view接收到layoutSubviews消息时(参见#1)。同样重要的是要理解,当调用它时,视图控制器view及其直接子视图已经布局,但更深的子视图尚未布局。(有关更全面的解释,请参阅此答案。)

另请注意,视图frame是根据其 的某些属性计算得出的layer:图层的positionbounds.sizeanchorPointtransform。如果有任何东西直接设置这些图层属性,则上述方法都不起作用。

由于这一切,当视图的位置发生变化时,没有特别好的方法可以得到通知。技术上正确的方法可能是使用CAAction. 当该层检测到其position正在发生变化时,它会向其委托(即视图本身,对于视图的层)询问要运行的操作。actionForLayer:forKey:它通过发送消息来询问。因此,您可以action(forLayer:forKey:)在视图子类中覆盖以得到可靠的通知。但是,该层在更改其(以及它的)之前要求执行操作。它更改其. 所以你必须像这样跳一段小舞:positionframeposition

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

执行正常返回UIView。但是在动画块内(包括当界面在纵向和横向之间旋转时),它返回(对于某些键)一个在层上安装 a 的动作。因此,如果有一个返回的动作,运行它是很重要的。actionForLayer:forKey:nilCAAnimationsuper


推荐阅读