简体   繁体   中英

layoutSubviews vs frame didSet to detect when the view's frame changed

Say I have a UIView subclass that needs to update something when its frame changes. Eg when it's on an even pixel, it'll be red, and blue when it's on an odd pixel.

I've seen at least four ways to do this:

  1. view.layoutSubviews() :

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

     override var frame: CGRect { didSet { backgroundColor = calculateColorFromFrame(frame) } } 
  3. Add a KVO observer on the view's frame.

  4. On the view's wrapping UIViewController, implement viewDidLayoutSubviews() .

     override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() view.updateBackgroundColorForNewFrame() } 
  5. Any other way you know of?

Which of these methods is preferred, and is one inherently better than the other? What's Apple's official recommendation?

  1. layoutSubviews : You can rely on this being called on a view whose size has changed, or if the view has received setNeedsLayout . If the view's position changes but its size doesn't change, the system doesn't automatically call layoutSubviews on that view.

  2. frame didSet : This works for a view that is laid out using autoresizingMask . It does not work for a view laid out using normal constraints. Instead, auto layout sets the view's center and bounds . However, these are implementation details subject to change in different versions of UIKit. I tested on the iOS 12.0 Simulator that comes with Xcode 10 beta 6.

  3. KVO on frame : This won't work in some circumstances for the same reason as #2. Also, frame is not documented to be KVO-compliant, so UIKit is allowed to change frame without notifying observers.

  4. viewDidLayoutSubviews : This can only work if the view is the view property of the view controller, and then only if the view receives the layoutSubviews message (see #1). Also it's important to understand that when this is called, the view controller's view and its direct subviews have been laid out, but deeper subviews have not been laid out yet. (See this answer for a fuller explanation.)

Note also that a view's frame is computed from some properties of its layer : the layer's position , bounds.size , anchorPoint , and transform . If anything sets those layer properties directly, none of the above methods will work.

Because of all this, there's no particularly great way to be notified when the view's position changes. The technically correct way is probably to use a CAAction . When the layer detects that its position is changing, it asks its delegate (which is the view itself, for a view's layer) for an action to run. It asks by sending the actionForLayer:forKey: message. So you can override action(forLayer:forKey:) in your view subclass to be notified reliably. However, the layer asks for the action before changing its position (and hence its frame ). It runs the action after changing its position . So you have to go through a little dance like this:

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

The UIView implementation of actionForLayer:forKey: normally returns nil . But inside an animation block (including when the interface is rotating between portrait and landscape), it returns (for some keys) an action that installs a CAAnimation on the layer. So it's important to run the action returned by super if there is one.

I might have understood your questions wrong or misinterpreted the Apple documentation, but here are my thoughts on what to use to see frame changes:

  1. You should not call view.layoutSubviews() directly. The LayoutSubviews method can be overridden by a subclass of UIView "only if the autoresizing and constraint-based behaviors of the subviews do not offer the behavior you want. You can use your implementation to set the frame rectangles of your subviews directly". - Copy from the Apple Documentation. I would therefore not use this method, since it doesn't apply to the view you want to change colors, but to the subviews of your view.
  2. I think this is a very elegant way to use, since if you move the view the frame will be set to new values. However, when the frame changes size, the didSet will also be called and there might not be a movement of the frame. If your calculateColorFromFrame(frame) is an expensive method and the view can change size, this might cause some performance loss.
  3. I think this is quite a cumbersome way to achieve the same as under point two. The only thing is that you now can use another class to observe changes in the view's frame and call the calculateColorFromFrame(frame) method.
  4. viewDidLayoutSubviews() in the respective view controller is also a good way, but here this method is called when something in the main view of that view controller has changed and the view has to lay out its sub views again. This also means that this method can be called when your view hasn't moved or changed and the calculateColorFromFrame(frame) did not change. This might therefore again call this method a couple of times to often.

What I'm curious about is how you're effectively are going to calculate the odd and even pixel, since the frame position and size is based on points and a point can span more than one pixel.

I hope this answered your question.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM