You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.20 11:15:42