跳到主要内容

自定义 View

问题

如何自定义 UIView?drawRect 的正确使用方式?UIView 和 CALayer 的关系是什么?

答案

UIView 绑定 CALayer

每个 UIView 都关联一个 CALayer,负责实际的渲染:

UIViewCALayer
事件处理内容绘制
响应者链渲染树
AutoLayoutCore Animation
手势识别位图缓存
// UIView 是 CALayer 的 delegate
view.layer.cornerRadius = 10
view.layer.shadowColor = UIColor.black.cgColor
view.layer.shadowOffset = CGSize(width: 0, height: 2)
view.layer.shadowOpacity = 0.3

自定义绘制

class PieChartView: UIView {
var percentage: CGFloat = 0.7

override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else { return }
let center = CGPoint(x: bounds.midX, y: bounds.midY)
let radius = min(bounds.width, bounds.height) / 2 - 10

// 背景圆
context.setFillColor(UIColor.systemGray5.cgColor)
context.addArc(center: center, radius: radius,
startAngle: 0, endAngle: .pi * 2, clockwise: false)
context.fillPath()

// 进度弧
context.setFillColor(UIColor.systemBlue.cgColor)
context.move(to: center)
context.addArc(center: center, radius: radius,
startAngle: -.pi / 2,
endAngle: -.pi / 2 + .pi * 2 * percentage,
clockwise: false)
context.closePath()
context.fillPath()
}

// 属性变化时触发重绘
func updatePercentage(_ value: CGFloat) {
percentage = value
setNeedsDisplay() // 标记需要重绘
}
}
draw(_:) 注意事项
  • 不要直接调用 draw(_:),而是用 setNeedsDisplay() 标记
  • draw(_:)主线程执行,复杂绘制会卡 UI
  • 每次调用会创建一个与 view 等大的位图(内存开销大
  • 简单圆角/边框用 layer 属性,不要重写 draw

测量 → 布局 → 绘制

class CustomView: UIView {
// 1. 告诉 AutoLayout 固有大小
override var intrinsicContentSize: CGSize {
return CGSize(width: 100, height: 44)
}

// 2. 布局子视图
override func layoutSubviews() {
super.layoutSubviews()
titleLabel.frame = CGRect(x: 16, y: 0,
width: bounds.width - 32,
height: bounds.height)
}

// 3. 绘制
override func draw(_ rect: CGRect) {
// 自定义绘制...
}
}

触摸处理

class DraggableView: UIView {
private var startPoint: CGPoint = .zero

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else { return }
startPoint = touch.location(in: superview)
}

override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else { return }
let currentPoint = touch.location(in: superview)
let dx = currentPoint.x - startPoint.x
let dy = currentPoint.y - startPoint.y
center = CGPoint(x: center.x + dx, y: center.y + dy)
startPoint = currentPoint
}
}

常见面试问题

Q1: setNeedsDisplay 和 setNeedsLayout 的区别?

答案

  • setNeedsDisplay:标记需要重绘(触发 draw(_:)
  • setNeedsLayout:标记需要重新布局(触发 layoutSubviews

都是异步的,在下一个 RunLoop 执行。对应的立即执行方法分别是 displayIfNeeded()layoutIfNeeded()

Q2: UIView 和 CALayer 的区别?

答案

  • UIView 负责事件处理(继承 UIResponder),CALayer 负责内容渲染
  • UIView 是 CALayer 的 delegate,布局 frame 实际委托给 layer
  • 纯展示(不需要事件)可以直接用 CALayer,更轻量
  • UIView 的动画底层通过 CALayer 的隐式动画实现

相关链接