UIView has an object property called Layer of type CALayer, and they behave similarly, with the main difference that CALayer inherits from NSObject and cannot respond to events.

This is because UIView is not only responsible for responding to events (inherited from UIReponder), but also a underlying encapsulation of CALayer. It can be said that their similar behavior depends on the implementation of CALayer, UIView just encapsulates its high-level interface.

So what is a CALayer?

CALayer (Layer)

The documentation defines it as: managing objects based on image content, allowing you to animate that content.

concept

Layers are usually used to provide backup storage for the view, but can also be used to display content without the view. The main job of a layer is to manage the visual content you provide, but the layer itself can set visual properties (such as background color, borders, and shadows). In addition to managing visual content, this layer also maintains information about content geometry (such as position, size, and transformation) that is used to display that content on the screen.

And UIView

Example 1-layer affects view changes:

let view = UIView(frame: CGRect(x: 44, y: 44, width: UIScreen.width - 88, height: 300))
view.backgroundColor = .red
view.layer.backgroundColor = UIColor.orange.cgColor

print("view: \(view.backgroundColor!)")
print("layer: \(view.layer.backgroundColor!)")

// Prints "view: 1 0.5 0 1"
// Prints "layer: 1 0.5 0 1"

view.layer.frame.origin.y = 100

print("view: \(view.frame.origin.y)")
print("layer: \(view.layer.frame.origin.y)")

// Prints "view: 100"
// Prints "layer: 100"
Copy the code

As you can see, the view changes whenever you modify the visual content or geometry of the layer, and vice versa. This proves that UIView depends on CALayer to be displayed.

Since their behavior is so similar, why not just use a UIView or CALayer to handle all events? Mainly based on two considerations:

  1. Different responsibilities

    The main job of UIVIew is to receive and respond to events; CALayer’s main job is to display the UI.

  2. Need to reuse

    On macOS and App systems, although NSView and UIView have similar behaviors, they are significantly different in implementation, but both rely on CALayer. In this case, only one CALayer can be encapsulated.

CALayerDelegate

You can use the Delegate (CALayerDelegate) object to provide the layer’s content, handle the layout of any sublayers, and provide custom actions in response to layer-related changes. If a layer is created by UIView, that UIView object is usually automatically specified as the layer delegate.

Note:

  1. In iOS, if the layer isUIViewObject association, this property must be set to own the layerUIViewObject.
  2. delegateJust another way for layers to handle content, not the only one.UIViewThe display of the layer has nothing to do with its layer delegate.
  1. func display(_ layer: CALayer)

    This method is called when a layer marks its contents as needing to be updated (setNeedsDisplay()). For example, set the contents property for the layer:

    let delegate = LayerDelegate(a)lazy var sublayer: CALayer = {
        let layer = CALayer()
        layer.delegate = self.delegate
        return layer
    }()
         
    // when 'sublayer.setneedsdisplay ()' is called, 'sublayer.display(_:)' is called.
    class LayerDelegate: NSObject.CALayerDelegate {
        func display(_ layer: CALayer) {
            layer.contents = UIImage(named: "rabbit.png")? .cgImage } }Copy the code

    So what is contents? Contents is defined to be of type Any, but in reality it only works on CGImage. The reason for this oddity is that, on macOS, it accepts both CGImage and NSImage objects.

    You can think of it as the image property in UIImageView, but actually, UIImageView internally assigns image.cgImage to contents by transforming it.

    Note:

    If it is a View layer, avoid setting the content of this property directly. The interaction between the view and the layer usually causes the view to replace the contents of this property during subsequent updates.

  2. func draw(_ layer: CALayer, in ctx: CGContext)

    Same as display(_:), but you can use the layer’s CGContext to implement the display(official example) :

    // sublayer.setNeedsDisplay()
    class LayerDelegate: NSObject.CALayerDelegate {
        func draw(_ layer: CALayer, in ctx: CGContext) {
            ctx.addEllipse(in: ctx.boundingBoxOfClipPath)
            ctx.strokePath()
        }
    }
    Copy the code
    • And the view ofdraw(_ rect: CGRect)The relationship between

      The documentation explains something like this:

      This method does nothing by default. Subclasses that draw view content using techniques such as Core Graphics and UIKit should override this method and implement their drawing code there. There is no need to override this method if the view sets its content in other ways. For example, if the view shows only the background color, or sets its content directly using the base layer object, etc.

      When you call this method, when you call this method, UIKit has already configured the drawing environment. Specifically, UIKit creates and configures the graphics context used to draw, and adjusts the transformation of that context so that its origin matches the origin of the view boundary rectangle. Can use UIGraphicsGetCurrentContext () function retrieves a reference to graphics context (not strong references).

      How does it create and configure the drawing environment? When I investigated their relationship, I found:

      /// Note: This method does nothing by default. It does not matter whether super.draw(_:) is called or not.
      override func draw(_ rect: CGRect) {
          print(#function)
      }
      
      override func draw(_ layer: CALayer, in ctx: CGContext) {
          print(#function)
      }
      
      // Prints "draw(_:in:)"
      Copy the code

      In this case, only the layer delegate method is output, and no view is displayed on the screen. Instead, call the layer’s super.draw(_:in:) method:

      /// Note: This method does nothing by default. It does not matter whether super.draw(_:) is called or not.
      override func draw(_ rect: CGRect) {
          print(#function)
      }
      
      override func draw(_ layer: CALayer, in ctx: CGContext) {
          print(#function)
          super.draw(layer, in: ctx)
      }
      
      // Prints "draw(_:in:)"
      // Prints "draw"
      Copy the code

      There’s a view on the screen, why? First of all, we want to know, in the calling view of the draw (_ : in:), it needs a carrier/panel/graphics context (UIGraphicsGetCurrentContext) for drawing operations. So I guess is that this UIGraphicsGetCurrentContext is in layer super. The draw (_ : in create and configure the inside:) method.

      The call sequence is as follows:

      1. Call the layer’s firstdraw(_:in:)Methods;
      2. Then, insuper.draw(_:in:)Method to create and configure the drawing environment;
      3. Layer by layersuper.draw(_:in:)Call the view ofdraw(_:)Methods.

      In addition, there is another situation:

      override func draw(_ layer: CALayer, in ctx: CGContext) {
          print(#function)
      }
      Copy the code

      Just implement the draw(_:in:) method for one layer and do not continue calling its super.draw(_:in:) to create the drawing environment. Can the view display without the drawing environment? The answer is yes! This is because: the view of the display is not dependent on the UIGraphicsGetCurrentContext, need only when drawing.

    • andcontentsThe relationship between

      Draw (_ rect: CGRect) is stored in the contents property of the layer:

      // ------ LayerView.swift ------
      override func draw(_ rect: CGRect) {
          UIColor.brown.setFill()	/ / fill
          UIRectFill(rect)
      
          UIColor.white.setStroke()	/ / stroke
          let frame = CGRect(x: 20, y: 20, width: 80, height: 80)
          UIRectFrame(frame)
      }
      
      // ------ ViewController.swift ------
      DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
          print("contents: \ [self.layerView.layer.contents)")}// Prints "Optional(<CABackingStore 0x7faf91f06e20 (buffer [480 256] BGRX8888)>)"
      Copy the code

      This is why CALayer provides the drawing environment, and why you need to be careful when introducing the contents property above.

      Important:

      If the delegate implements display(_ :), this method will not be called.

    • anddisplay(_ layer: CALayer)The relationship between

      As mentioned earlier, the View’s draw(_:) method is called by its layer’s draw(_:in:) method. But what if we implement display(_:) instead of draw(_:in:)? This means that draw(_:in:) has lost its usefulness. The view will not be displayed on the screen without context support, and display(_:) will not automatically call the view’s draw(_:), The view draw(_:) method also loses its meaning, so what is the use of display(_ layer: CALayer)? Such as:

      override func draw(_ rect: CGRect) {
          print(#function)
      }
      
      override func display(_ layer: CALayer) {
          print(#function)
      }
      
      // Prints "display"
      Copy the code

      Here draw(_:) is not called and the associated view is not displayed on the screen. Func display(_ layer: CALayer); background color can be set as well. Of course, you’ll probably never do that unless you create a separate layer.

      As for why it doesn’t call its parent implementation from inside the display(_ layer: CALayer) method, it crashes if it does:

      // unrecognized selector sent to instance 0x7fbcdad03ba0
      Copy the code

      And why? According to my references, none of them continue to call the super (UIView) method here. My casual guess is this:

      Unrecognized selector (method) sent to instance. So let’s analyze, in which case? What is it?

      1. issuperInstance;
      2. isdisplay(_ layer: CALayer)

      That is, when the super.display(_ layer: CALayer) method is called, it is not found in super. Why is that? Note that UIView already follows the CALayerDelegate protocol by default (right-click UIView to view the header file), but it should not implement its display(_:) method, choosing instead to leave it to subclasses. A similar implementation would be:

      / / hint ` CALayerDelegate `
      @objc protocol LayerDelegate: NSObjectProtocol {
          @objc optional func display(a)
          @objc optional func draw(a)
      }
      
      / / hint ` CALayer `
      class Layer: NSObject {
          var delegate: LayerDelegate?
      }
      
      / / hint ` UIView `
      class BaseView: NSObject.LayerDelegate {
          let layer = Layer(a)override init() {
              super.init()
              layer.delegate = self}}// Note that the delegate 'display()' method is not implemented.
      extension BaseView: LayerDelegate {
          func draw(a){}}// Indicate a subclass of UIView
      class LayerView: BaseView {
          func display(a) {
              // The same code can be implemented on OC without problem.
              // Since Swift is statically compiled, it checks for this method in the 'BaseView' class,
              // If not, a compilation error is displayed.
              super.display()
          }
      }
      
      // ------ ViewController.swift ------
      let layerView = LayerView(a)// A crash will occur if 'super.display()' is called inside the method.
      layerView.display()
      // Normal execution
      layerView.darw()
      Copy the code

    Note:

    Only when the system detects that the view’s draw(_:) method has been implemented will the layer’s display(_:) or draw(_ rect: CGRect) methods be automatically called. Otherwise it must be called manually by calling the layer’s setNeedsDisplay() method.

  3. func layerWillDraw(_ layer: CALayer)

    Called before draw(_ Layer: CALayer, in CTX: CGContext), you can use this method to configure the state of any layers that affect the content (such as contentsFormat and isOpaque).

  4. func layoutSublayers(of layer: CALayer)

    Similar to UIView layoutSubviews(). This method is called when a boundary is found to have changed and its sublayers may need to be rearranged (for example, by resizing a frame).

  5. func action(for layer: CALayer, forKey event: String) -> CAAction?

    CALayer is able to perform animations because it is defined in the Core Animation framework and is the Core of the operations that Core Animation performs. In other words, CALayer is not only responsible for displaying content, but also for performing animations (in fact, the operations between Core Animation and hardware are performed, and CALayer is responsible for storing the data needed for the operations, similar to Model). Therefore, most properties that use CALayer are attached to animations. But in UIView, this effect is turned off by default. You can turn it back on using its layer delegate method (also automatically turned on in the View Animation block) and return the object that determines its animation effect. If it returns nil, the default implicit animation effect will be used.

    Example – Using layer’s delegate method returns a basic animation of moving an object from left to right:

    final class CustomView: UIView {
        override func action(for layer: CALayer, forKey event: String) -> CAAction? {
            guard event == "moveRight" else {
                return super.action(for: layer, forKey: event)
            }
            let animation = CABasicAnimation()
            animation.valueFunction = CAValueFunction(name: .translateX)
            animation.fromValue = 1
            animation.toValue = 300
            animation.duration = 2
            return animation
        }
    }
    
    let view = CustomView(frame: CGRect(x: 44, y: 44, width: UIScreen.width - 88, height: 300))
    view.backgroundColor = .orange
    self.view.addSubview(view)
    
    let action = view.layer.action(forKey: "moveRight") action? .run(forKey:"transform", object: view.layer, arguments: nil)
    Copy the code

    How do you know which of its properties can be animated? The Core animation programming guide lists the CALayer properties you may need to consider setting up animations:

CALayer coordinate system

CALayer has position properties other than frame and bounds that are distinct from UIView. UIView uses properties called frame, Bounds, center, etc. that are returned from CALayer, and frame is just a computational property in CALayer.

AnchorPoint and position in CALayer are also the main dependencies in CALayer coordinate system:

  • Var anchorPoint: CGPoint

    Diagram of layer anchor points

    Just look at the iOS section. As you can see, the anchor point is based on the layer’s internal coordinates, which range from (0-1, 0-1), and you can think of it as the scale factor of the bounds. The middle ones (0.5, 0.5) are the default anchorPoint for each layer; The upper left (0.0, 0.0) is considered the starting point of anchorPoint.

    Any layer-based geometry takes place near the specified point. For example, applying the rotation transform to a layer with a default anchor point causes rotation around its center, and changing the anchor point to another position causes the layer to rotate around that new point.

    Anchor points affect the layer transformation diagram
  • Var position: CGPoint (anchor position)

    Anchors affect the position diagram of the layer

    Just look at the iOS section. Position in Figure 1 is marked as (100, 100).

    For anchor points, it has more detailed coordinates in the parent layer. Position is the position of the anchor point on the parent layer.

    The default anchor point of a layer is (0.5, 0.5). In this case, let’s look at the position of anchor point X on the parent layer. And y is similar, from parent layer Y to anchor point Y is also 100; Then, it can be concluded that the coordinate of the anchor point in the parent layer is (100, 100), which is the value of position in this layer.

    The same is true for Figure 2, where the anchor point is in the starting position (0.0, 0.0) and the position from parent layer X to anchor point x is 40. The position from parent layer Y to anchor point Y is 60, so the value of position in this layer is (40, 60).

    In fact, there is a formula for calculating position. The following formula can be applied according to Figure 1:

    1. Position. x = frame.origin.x + 0.5 * bounds.size.width
    2. Position. Y = frame.origin. Y + 0.5 * bounds.size.height

    Since 0.5 is the default value for anchorPoint, a more general formula would be:

    1. position.x = frame.origin.x + anchorPoint.x * bounds.size.width
    2. position.y = frame.origin.y + anchorPoint.y * bounds.size.height

    Note:

    In fact, position is the center in UIView. If we change position on the layer, the view’s center will change, and vice versa.

The relationship between anchorPoint and Position

As mentioned earlier, position is in the anchor position (relative to the parent layer). There is a question here, that is, since position is relative to anchorPoint, will the position change if the anchorPoint is modified? The conclusion is no:

let redView = UIView(frame: CGRect(x: 40, y: 60, width: 120, height: 80))
print(self.redView.layer.position)	/ / Prints "(100.0, 100.0)"
redView.layer.anchorPoint = CGPoint(x: 0, y: 1)
print(self.redView.layer.position)	/ / Prints "(100.0, 100.0)"
Copy the code

Does changing position change anchorPoint? The conclusion is that neither:

let redView = UIView(frame: CGRect(x: 40, y: 60, width: 120, height: 80))
print(redView.layer.anchorPoint)	/ / Prints "(0.5, 0.5)"
redView.layer.anchorPoint = CGPoint(x: 0, y: 1)
print(redView.layer.anchorPoint)	/ / Prints "(0.5, 0.5)"
Copy the code

After testing, no matter who changes are made, the other side is not affected, only frame. Origin is affected. As to why they do not affect each other, I do not know yet. My casual guess is this:

AnchorPoint is actually anchorPoint; Position is position. In fact, they are not related, because their default positions overlap, which leads to the misconception that position must be the same point as anchorPoint.

And frame

CALayerframe 在The documentIs described as a computational property fromboundsanchorPointpositionDerived from the value of. When you specify a new value for this property, the layer changes itpositionboundsProperty to match the rectangle you specify.

So how do they determine the frame? The following formula can be applied according to the picture:

  1. frame.x = position.x - anchorPoint.x * bounds.size.width
  2. frame.y = position.y - anchorPoint.y * bounds.size.height

This explains why changing position and anchorPoint causes the frame to change. We can test this by assuming the anchorPoint is in the lower left corner (0.0, 1.0) :

let redView = UIView(frame: CGRect(x: 40, y: 60, width: 120, height: 80))
redView.layer.anchorPoint = CGPoint(x: 0, y: 1)
print(redView.frame.origin)	/ / Prints "(100.0, 20.0)"
Copy the code

Frame. x (100) = 100-0 * 120, frame.y (20) = 100-1 * 80; It matches the printed results. On the other hand, modifying the position property also causes the frame. Origin to change as a formula, which I won’t go into here.

Note:

Changing the value of frame will cause position to change because position is defined based on the parent layer. Changing the frame means that its position in the parent layer changes, and position changes accordingly.

But changing the frame does not change anchorPoint, because anchorPoint is defined on its own layer and does not change no matter what the outside changes.

Confusion caused by modifying anchorPoint

To change position is to change its “center”, which is easy to understand. However, for modifying anchorPoint, I believe many people have had the same confusion. Why do the changes brought by modifying anchorPoint often differ from what they imagined? Let’s look at an example of modifying anchor point x (0.5 → 0.2) :

A closer look at “Figure 2” shows that neither the new nor the old anchors have changed their position in their own layer. Since the anchor point itself doesn’t change, the only thing that changes is x. How does x change? It is clear from the picture that the new anchor point is moved to the same position as the old one. This is also a misunderstanding of most people. They think that modifying 0.5 -> 0.2 is to move the old anchorPoint to the new one, but the result is just the opposite. This is the reason why modifying the anchorPoint is often different from what they expected.

Another way to think about it is to imagine that the red layer at the bottom of “Figure 1” is a piece of paper, and the white dot in the middle is like a tack in the middle. As you move, you hold the tack in the middle to keep it still. Now suppose you were to start moving to any point, what would you do? The only way to do this is to move the entire layer so that the new anchor points are in the same position as the old one until they are completely on top of each other.

reference

Thoroughly understand position and anchorPoint

Core animation programming guide

IOS Core Animation: Advanced Techniques

Apple UIView document

Apple CALayer documentation