CGContext — Core Graphics 中用于在 iOS 和 macOS 上执行 2D 绘图命令的主要对象。它代表一个图形上下文,存储当前状态:填充颜色、线条粗细、变换、裁剪区域和字体。所有绘图操作——线条、矩形、文本、图像——都通过 CGContext 执行。根据 Apple Developer Documentation, 2026,CGContext 是 Apple 生态系统中所有栅格和矢量图形的基础。
要点
CGContext — Core Graphics 框架中的一个不透明类型(opaque type),代表执行 2D 渲染命令的环境。它管理状态堆栈、当前色彩空间、坐标变换和裁剪区域。CGContext 是所有 Quartz 2D 绘图函数交互的唯一对象——任何图形操作都通过它进行。
CGContext 根据用途有多种变体:bitmap context 用于在内存中处理栅格图像,PDF context 用于生成 PDF 文档,window context 用于屏幕渲染(由 UIView 在 draw(_:) 中提供)。在 iOS 中,开发者最常使用从 UIGraphicsGetCurrentContext() 在 draw(_:) 方法中获取的上下文,或从 UIGraphicsBeginImageContextWithOptions 获取的离线渲染上下文。
上下文通过状态堆栈应用所有设置——开发者可以保存当前状态(saveGState),修改参数,然后恢复之前的状态(restoreGState)。这在绘制复杂场景时至关重要,其中不同元素需要不同的颜色、线条粗细和变换。根据 Apple WWDC 2023,正确使用状态堆栈可以提高代码可读性并防止视觉伪影。
CGContextSaveGState 将当前上下文状态的副本压入堆栈。此状态包括所有设置:颜色、线条粗细、变换、阴影、裁剪区域、字体。修改参数后,调用 CGContextRestoreGState 将上下文恢复到保存的状态,撤销所有中间更改。在 iOS 上,堆栈最多可包含 32 个状态。
在绘制具有不同样式的一组元素时(例如图表的彩色扇区),状态堆栈消除了手动记住和恢复每个参数的需要。如果没有 saveGState/restoreGState,开发者必须在每个元素之后重新设置颜色、阴影和变换。使用堆栈可以简化代码并避免恢复错误。
保持保存和恢复的平衡很重要——每个 saveGState 必须有一个配对的 restoreGState。破坏平衡会导致状态泄漏或过早恢复,从而引起不可预测的显示。Xcode 静态分析器不检查 CGContext 堆栈平衡,因此正确性的责任在于开发者。建议将配对分组到 do/finally 块中。
override func draw(_ rect: CGRect) {
guard let ctx = UIGraphicsGetCurrentContext() else { return }
ctx.saveGState()
ctx.setStrokeColor(UIColor.red.cgColor)
ctx.setLineWidth(3)
ctx.addRect(CGRect(x: 50, y: 50, width: 200, height: 100))
ctx.strokePath()
ctx.restoreGState()
}
CGContextSetFillColorWithColor 设置后续操作的填充颜色。颜色作为 CGColorRef 传递,包含有关色彩空间和颜色分量的信息。CGContextSetStrokeColorWithColor 类似地用于描边颜色。Apple 建议缓存经常使用的 CGColorRef 以提高性能。
CGContextDrawLinearGradient 在两点之间绘制带有指定颜色的线性渐变。CGContextDrawRadialGradient 创建从一个圆过渡到另一个圆的径向渐变。使用渐变时,首先创建一个包含颜色数组和停止位置的 CGGradientRef 对象。CGContext 中的渐变支持 RGB 和 Grayscale 色彩空间。
用渐变填充区域需要预先设置裁剪区域。如果没有 clip,渐变将填充整个上下文,而不仅仅是所需的形状。典型模式:将路径添加到上下文,调用 clip 限制区域,然后绘制渐变。此模式用于所有需要复杂形状渐变填充的应用程序中,从按钮到背景。
| 函数 | 用途 | 色彩空间 |
|---|---|---|
| setFillColor | 设置填充颜色 | 任意(通过 CGColor) |
| setStrokeColor | 设置描边颜色 | 任意(通过 CGColor) |
| drawLinearGradient | 绘制线性渐变 | RGB 或 Grayscale |
| drawRadialGradient | 绘制径向渐变 | RGB 或 Grayscale |
CGContextAddPath 将先前创建的 CGPathRef 添加到上下文以供后续绘制。添加路径后,可以描边(strokePath)、填充(fillPath)或执行两种操作。CGContextBeginPath 开始新路径,清除之前的路径。上下文在任意时刻只支持一个活动路径。
CGContextMoveToPoint 将路径的当前点移动到指定坐标。CGContextAddLineToPoint 从当前点到指定点绘制一条直线。CGContextAddCurveToPoint 添加三次贝塞尔曲线,CGContextAddQuadCurveToPoint 添加二次曲线。CGContextClosePath 用一条直线连接最后一点和第一点来闭合路径。
与 UIBezierPath 不同的是,所有这些方法在路径对象上调用,而在 CGContext 中它们直接在上下文上调用。这种区别在选择方法时很重要:UIBezierPath 更适合重用轮廓,CGContext 更适合以最小开销进行一次性绘制。CGContext 还通过 CGContextSetLineDash 支持虚线,通过 CGContextSetLineCap 支持灵活端点,通过 CGContextSetLineJoin 支持连接。
let ctx = UIGraphicsGetCurrentContext()!
ctx.beginPath()
ctx.moveTo(x: 50, y: 100)
ctx.addLineTo(x: 200, y: 100)
ctx.addCurveTo(x1: 250, y1: 50, x2: 300, y2: 150, x3: 350, y3: 100)
ctx.setStrokeColor(UIColor.blue.cgColor)
ctx.setLineWidth(5)
ctx.strokePath()
CGContextClip 将绘制区域限制在当前轮廓内。所有后续绘制操作仅在此区域内可见。这是遮罩、圆角和非标准元素边界的基本机制。裁剪区域也通过 saveGState/restoreGState 保存和恢复。
CGContextTranslateCTM 平移上下文坐标原点。CGContextScaleCTM 缩放 X 和 Y 轴。CGContextRotateCTM 围绕当前原点旋转坐标系。变换是累积的——每个后续变换都相对于当前坐标系应用。复合变换(平移 + 旋转 + 缩放)用于动画和元素定位。
裁剪和变换的结合使用提供了强大的功能:可以通过裁剪创建复杂的遮罩,然后对整个内容应用变换。典型示例——以一定角度显示带有圆角的图像部分。操作顺序很重要:首先设置裁剪区域,然后应用变换,最后绘制内容。Apple 建议在绘制循环中尽量减少 CTM(Current Transformation Matrix)的更改次数。
override func draw(_ rect: CGRect) {
guard let ctx = UIGraphicsGetCurrentContext() else { return }
ctx.saveGState()
// 裁剪区域 - 圆形
ctx.addEllipse(in: CGRect(x: 50, y: 50, width: 200, height: 200))
ctx.clip()
// 内容变换
ctx.translateBy(x: 150, y: 150)
ctx.rotate(by: .pi / 4)
ctx.translateBy(x: -150, y: -150)
UIImage(named: "photo")?.draw(at: CGPoint(x: 50, y: 50))
ctx.restoreGState()
}
让我们看一个使用 CGContext 创建渐变图表的完整示例。该图表包括三个不同颜色的扇区,带有渐变填充和文本标签。代码演示了在单个上下文中 saveGState、clip、渐变和文本渲染的组合。
class PieChartView: UIView {
private struct Slice {
let color: UIColor
let value: CGFloat
let label: String
}
override func draw(_ rect: CGRect) {
guard let ctx = UIGraphicsGetCurrentContext() else { return }
let slices = [
Slice(color: .systemRed, value: 0.4, label: "iOS"),
Slice(color: .systemGreen, value: 0.35, label: "Android"),
Slice(color: .systemBlue, value: 0.25, label: "Other")
]
let center = CGPoint(x: bounds.midX, y: bounds.midY)
let radius = bounds.width * 0.35
var startAngle: CGFloat = -.pi / 2
for slice in slices {
let endAngle = startAngle + .pi * 2 * slice.value
ctx.saveGState()
ctx.moveTo(x: center.x, y: center.y)
ctx.addArc(center: center, radius: radius,
startAngle: startAngle, endAngle: endAngle, clockwise: false)
ctx.closePath()
ctx.setFillColor(slice.color.cgColor)
ctx.fillPath()
// 文本标签
let midAngle = startAngle + endAngle / 2
let labelPos = CGPoint(
x: center.x + radius * 0.7 * cos(midAngle),
y: center.y + radius * 0.7 * sin(midAngle)
)
slice.label.draw(at: labelPos, withAttributes: [
.foregroundColor: UIColor.white,
.font: UIFont.boldSystemFont(ofSize: 14)
])
ctx.restoreGState()
startAngle = endAngle
}
}
}
在此示例中,图表的每个扇区在自己的 saveGState/restoreGState 块中绘制。这种方法确保一个扇区的颜色和路径设置不会影响其他扇区。addArc 方法从起始角度到结束角度创建弧线,closePath 将扇区闭合到中心。文本标签使用三角函数 cos 和 sin 计算位置,放置在扇区角度的中线上。
为提高频繁重绘时的性能,建议将角度和位置的计算缓存到单独的数组中。CGContext 在 CPU 上执行所有操作,因此具有数十个扇区的复杂场景可能需要优化——例如在 CGLayer 中预栅格化或使用 Metal 进行 GPU 加速。对于大多数具有 5-10 个元素的 UI 任务,CGContext 的性能仍然可以接受。
常见问题
在 UIView 的 draw(_:) 方法内部或 UIGraphicsBeginImageContextWithOptions 块内部调用 UIGraphicsGetCurrentContext()。对于 CALayer 图层,使用 CALayerDelegate 代理的 draw(in:),它会接收一个准备好的上下文。
fillPath 用 setFillColor 的颜色填充当前轮廓的内部区域。strokePath 使用 setStrokeColor、setLineWidth 和 setLineCap 参数沿轮廓绘制线条。两种操作顺序执行:先 fill,必要时再 stroke。
Bitmap CGContext — 通过 CGBitmapContextCreate 创建的上下文,它绘制到内存缓冲区而不是屏幕。用于后台图像生成、无显示图形处理以及为 Metal 或 OpenGL 创建纹理。
Core Graphics 不提供重置所有设置的方法。在更改前使用 saveGState,然后使用 restoreGState 返回到原始状态。或者,可以使用相同参数创建新的上下文。
在调用 drawLinearGradient 之前,通过 clip 在所需轮廓上设置裁剪区域。没有 clip,渐变会填充整个上下文。顺序:addPath → clip → drawLinearGradient — 确保渐变在形状内部。
总结
我们将开发一款交钥匙移动应用程序
IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。