自定义 View
问题
如何自定义 UIView?drawRect 的正确使用方式?UIView 和 CALayer 的关系是什么?
答案
UIView 绑定 CALayer
每个 UIView 都关联一个 CALayer,负责实际的渲染:
| UIView | CALayer |
|---|---|
| 事件处理 | 内容绘制 |
| 响应者链 | 渲染树 |
| AutoLayout | Core 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 的隐式动画实现