如何在SwiftUI Canvas/TimelineView中持续绘图而不清空上一帧
使用SwiftUI Canvas+Timeline实现累积绘图(类似p5.js不清空背景效果)
问题描述
我希望使用SwiftUI的Canvas和Timeline视图绘图,但不希望每一帧都清空绘图上下文。在下方示例中,我想持续在屏幕绘制随机点,最终累积成全随机像素图像,但实际仅显示一个随机颜色和位置的像素。
这是演示问题的简单示例,我不需要替代方案(如用常规SwiftUI视图绘制Rect),希望通过Canvas和Timeline解决,这更符合我一系列项目的需求。
本质上,我想实现类似p5.js中不清除背景时draw函数的效果。
问题代码
struct RandomPoints: View { let colors : [Color] = [ .red, .orange, .yellow, .green, .mint, .teal, .cyan, .blue, .indigo, .purple, .pink, .brown ] var body: some View { ZStack { Color.black TimelineView(.animation) { timeline in Canvas { context, size in // Accessing the timeline inside Canvas is what performs the animation let _ = timeline.date.timeIntervalSinceReferenceDate context.fill(Path(CGRect(x: Double.random(in: 0..<640.0), y: Double.random(in: 0..<480.0), width: 2, height: 2)), with: .color(colors.randomElement()!)) } .frame(width: 640.0, height: 480.0) } } } }
解决方案
SwiftUI的Canvas是即时模式绘图,每帧都会从头重绘,不会保留上一帧的上下文内容。要实现累积绘图,核心是自己存储所有绘制元素的历史数据,或者使用离屏缓存来保存已绘制内容。以下提供几种可行方案:
方案1:存储所有绘制点数据(基础版)
通过@State维护所有点的集合,每帧添加新点后重绘全部历史点:
// 定义存储点信息的结构体 struct PointData: Identifiable { let id = UUID() let rect: CGRect let color: Color } struct RandomPoints: View { let colors: [Color] = [.red, .orange, .yellow, .green, .mint, .teal, .cyan, .blue, .indigo, .purple, .pink, .brown] // 用@State保存所有已绘制的点 @State private var points: [PointData] = [] var body: some View { ZStack { Color.black TimelineView(.animation) { timeline in Canvas { context, size in // 生成新的随机点并添加到集合 let newPoint = PointData( rect: CGRect( x: Double.random(in: 0..<size.width), y: Double.random(in: 0..<size.height), width: 2, height: 2 ), color: colors.randomElement()! ) points.append(newPoint) // 遍历所有点进行绘制 for point in points { context.fill(Path(point.rect), with: .color(point.color)) } } .frame(width: 640.0, height: 480.0) } } } }
方案2:使用Observable优化数据管理(iOS 17+)
对于iOS 17及以上版本,使用@Observable可以更高效地管理数据,避免数组频繁更新带来的性能损耗:
import SwiftUI import Observation // 用Observable对象管理点集合 @Observable class PointStore { var points: [PointData] = [] } struct PointData: Identifiable { let id = UUID() let rect: CGRect let color: Color } struct RandomPoints: View { let colors: [Color] = [.red, .orange, .yellow, .green, .mint, .teal, .cyan, .blue, .indigo, .purple, .pink, .brown] @State private var pointStore = PointStore() var body: some View { ZStack { Color.black TimelineView(.animation) { timeline in Canvas { context, size in let newPoint = PointData( rect: CGRect( x: Double.random(in: 0..<size.width), y: Double.random(in: 0..<size.height), width: 2, height: 2 ), color: colors.randomElement()! ) pointStore.points.append(newPoint) for point in pointStore.points { context.fill(Path(point.rect), with: .color(point.color)) } } .frame(width: 640.0, height: 480.0) } } } }
方案3:离屏缓存优化性能(适合大量绘制场景)
当需要绘制大量元素时,重复遍历所有点会导致性能下降,此时可以使用CGImage作为离屏缓存,每次将新内容绘制到缓存中,再在Canvas里渲染缓存图像:
struct RandomPoints: View { let colors: [Color] = [.red, .orange, .yellow, .green, .mint, .teal, .cyan, .blue, .indigo, .purple, .pink, .brown] @State private var offscreenImage: CGImage? let canvasSize = CGSize(width: 640, height: 480) var body: some View { ZStack { Color.black TimelineView(.animation) { timeline in Canvas { context, size in // 配置图像渲染器 let format = UIGraphicsImageRendererFormat() format.scale = 1 let renderer = UIGraphicsImageRenderer(size: canvasSize, format: format) // 更新离屏缓存:先绘制已有内容,再添加新点 offscreenImage = renderer.image { rendererContext in if let cachedImage = offscreenImage { rendererContext.cgContext.draw(cachedImage, in: CGRect(origin: .zero, size: canvasSize)) } // 绘制新的随机点 let newRect = CGRect( x: Double.random(in: 0..<canvasSize.width), y: Double.random(in: 0..<canvasSize.height), width: 2, height: 2 ) colors.randomElement()!.setFill() rendererContext.fill(newRect) }.cgImage // 在Canvas中渲染离屏缓存 if let image = offscreenImage { context.draw(image, in: CGRect(origin: .zero, size: canvasSize)) } } .frame(width: canvasSize.width, height: canvasSize.height) } } } }
核心原理说明
- SwiftUI Canvas默认是即时渲染,每帧都会重置绘图上下文,因此必须手动保存所有需要保留的绘制内容。
TimelineView(.animation)会以屏幕刷新率触发回调,确保每帧都能添加新的绘制元素。- 三种方案分别对应不同场景:基础版适合少量元素,Observable版适合iOS 17+的高效数据管理,离屏缓存版适合大量元素的性能优化。
内容的提问来源于stack exchange,提问作者Brett
相关产品推荐
相关产品推荐

