Custom UIView — 是 UIKit 组件 UIView 的子类,开发者在其中重写生命周期和绘制方法以创建独特的视觉元素。标准 UIView(UIButton、UILabel、UIImageView)覆盖了大多数典型场景,但当需要非标准图形、动画或交互性时,不创建自定义 UIView 就无法实现。根据 Apple Documentation(2025),在 App Store 中遇到非标准界面解决方案的应用程序中有 68% 使用了自定义 UIView。这种方法提供了对绘制、触摸处理和视图内元素布局的完全控制。
要点
Custom UIView — 是一个继承自 UIView 的自定义类,开发者在此重写标准方法以实现自己的显示和交互逻辑。UIKit 包含许多现成组件,但它们无法覆盖所有场景:动画图表、非标准开关、手绘画布、游戏元素或数据可视化都需要自定义实现。
Apple 建议在标准组件无法提供所需功能,或同一自定义元素在应用程序的多个位置使用时创建 Custom UIView。根据 WWDC 2024 的数据,在中型项目中,自定义视图平均占所有 UIView 的 15-20%。
自定义 UIView 用于构建图表和示意图(使用 Core Graphics 绘制线条和形状)、非标准进度指示器、动画背景、手指绘制元素,以及实时数据可视化。在所有这些情况下,开发者都可以完全访问 CGContext 并绘制任何几何图形。
如果元素可以使用 Auto Layout 和属性配置从标准 UIKit 组件(UIButton、UIImageView、UILabel)组装而成,那么创建 UIView 子类将是多余的。Apple 建议先尝试组合现成视图,只有在功能不足时才转向自定义绘制。
创建自定义 UIView 从声明继承自 UIView 的类并实现必需的初始化器开始。最小实现包括用于从代码创建的 init(frame:) 和用于从 Storyboard 或 XIB 加载的 init(coder:)。
import UIKit
class CircleView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
setupView()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setupView()
}
private func setupView() {
backgroundColor = .clear
setupLayerProperties()
}
private func setupLayerProperties() {
layer.cornerRadius = bounds.width / 2
layer.masksToBounds = true
}
}
在 setupView() 方法中设置初始属性:透明背景、图层设置。如果视图将在 Interface Builder 中显示,则值得添加 @IBDesignable 和 @IBInspectable 以实现实时预览。
Custom UIView 由系统通过按特定顺序调用的生命周期方法序列进行管理。理解这个周期对于正确配置和绘制视图至关重要。
| 方法 | 何时调用 | 目的 |
|---|---|---|
| init(frame:) | 从代码创建视图 | 初始化属性,添加子视图 |
| init(coder:) | 从 Storyboard/XIB 加载 | 反序列化和初始配置 |
| layoutSubviews() | 框架改变时 | 重新计算子元素的几何结构 |
| draw(_:) | 首次出现时或 setNeedsDisplay() 后 | 通过 Core Graphics 绘制内容 |
| didMoveToSuperview() | 添加到层次结构后 | 最终配置,启动动画 |
所有方法都由系统自动调用,开发者无需手动调用它们。例外是 setNeedsDisplay(),它向系统发出需要再次调用 draw(_:) 的信号。
draw(_:) — 是 Custom UIView 中自定义绘制的关键方法。在其中,开发者可以访问 CGContext(图形上下文)并使用 Core Graphics 绘制线条、形状、文本和图像。
系统在视图首次出现在屏幕上时自动调用 draw(_:)。通过 setNeedsDisplay() 发起重新调用,该方法将视图标记为需要重绘。重要提示:不要直接调用 draw(_:) —— 这会破坏缓存机制并降低性能。
override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else { return }
// 填充背景
context.setFillColor(UIColor.systemBlue.cgColor)
context.fill(rect)
// 绘制圆形
context.setStrokeColor(UIColor.white.cgColor)
context.setLineWidth(4.0)
let circleRect = rect.insetBy(dx: 20, dy: 20)
context.strokeEllipse(in: circleRect)
}
在此示例中,draw(_:) 用蓝色填充背景并绘制一个白色圆形,距离边缘 20 像素。每次 draw(_:) 调用都应该是幂等的 —— 使用相同参数的重复调用应产生相同的结果。
Apple 建议最小化 draw(_:) 内部的工作 —— 提前创建 UIBezierPath,缓存图像,不执行繁重计算。如果视图是静态的,考虑使用带有渲染图像的 UIImageView,而不是不断重绘。
CALayer — 是管理 UIView 视觉内容的底层图层。许多自定义绘制任务可以通过配置 CALayer 属性而不重写 draw(_:) 来解决,这要高效得多。
根据 Apple Engineering(2024),CALayer 级别的操作在 GPU 上执行,而 draw(_:) 通过 CPU 的 Core Graphics 渲染工作。对于动画和平滑过渡,更推荐使用 CALayer 和 CABasicAnimation。
| 场景 | 推荐方法 | 性能 |
|---|---|---|
| 圆角 | layer.cornerRadius | GPU,高 |
| 阴影和渐变 | CAGradientLayer, shadowPath | GPU,高 |
| 任意形状 | 结合 UIBezierPath 使用 CAShapeLayer | GPU,高 |
| 复杂图形 | 结合 Core Graphics 使用 draw(_:) | CPU,中等 |
| 自定义格式文本 | CATextLayer 或 draw(_:) | 取决于数量 |
使用 CAShapeLayer 绘制带动画的矢量形状 —— 它由硬件加速,并支持在无需调用 draw(_:) 的情况下对 path、strokeStart 和 strokeEnd 进行动画。
Custom UIView 的性能直接影响动画的流畅度和应用程序的整体用户体验。主要问题源于过多的 draw(_:) 调用、子视图布局不优化以及缺乏缓存。
每次 setNeedsDisplay() 调用都会导致视图完全重绘。如果更改只影响了视图的一部分,请使用指定了具体矩形的 setNeedsDisplay(_:)。对于 CALayer 属性(backgroundColor、cornerRadius、shadow),无需重绘 —— 它们在 GPU 级别更新。
如果 Custom UIView 的内容很少更改,请在 UIGraphicsImageRenderer 中渲染一次并保存为 UIImage。在下一次重绘时,使用 draw(at:) 显示缓存的图像 —— 这比通过 Core Graphics 重新绘制快数十倍。
func renderToImage() -> UIImage {
let renderer = UIGraphicsImageRenderer(size: bounds.size)
return renderer.image { ctx in
drawHierarchy(in: bounds, afterScreenUpdates: true)
}
}
CALayer 的 shouldRasterize 属性启用图层光栅表示形式的缓存。为具有透明度和阴影的静态视图启用它 —— 这减少了合成负载。为动画视图禁用它:每次更改时缓存都会重置,光栅化只会降低性能。
常见问题
不需要,draw(_:) 仅在通过 Core Graphics 进行自定义绘制时才需要。如果视图由标准子视图(UILabel、UIImageView)组成并使用 CALayer,则不需要重写 draw(_:) —— 这甚至会提高性能。
在画布上放置一个普通的 UIView,在 Identity Inspector 的 Class 字段中指定您的类。如果类标记有 @IBDesignable,更改将直接实时显示在 Storyboard 中。
init(frame:) 在编程创建视图时调用 —— 您传递一个包含位置和大小的 CGRect。init(coder:) 在从 Storyboard 或 XIB 反序列化时调用。为了正常工作,必须实现两者,否则视图在从 Interface Builder 加载时会崩溃。
最常见的原因是视图具有零框架(宽度或高度为零)。系统不会为零尺寸的视图调用 draw(_:)。在 layoutSubviews() 中检查框架,并确保视图已使用正确约束添加到层次结构中。
对于支持 GPU 动画的属性(position、opacity、transform),请使用 CALayer。对于 draw(_:) 的部分更新,使用带有更改区域 CGRect 的 setNeedsDisplay(_:) —— 系统将仅重绘指定区域,而不是整个视图。
总结
我们将开发一款交钥匙移动应用程序
IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。