在Tokamak中实现视图状态变更时Canvas自动重绘的最优方案问询
可靠触发Canvas重绘的Tokamak原生方案
你当前用setTimeout加手动订阅objectWillChange的方式确实是临时hack,容易带来时序问题和维护成本。其实Tokamak本身提供了更贴合其状态管理流程的解决方案,既能让框架负责状态更新,又能精准触发Canvas重绘。
核心思路
放弃手动订阅ObservableObject的变更事件,转而利用Tokamak的视图生命周期修饰符onChange,它会在状态更新完成且DOM同步后触发回调,完全不需要setTimeout这种延迟手段。同时,我们可以直接用DynamicHTML创建<canvas>元素,避免嵌套div的额外操作,还能解决Tokamak重置画布尺寸的问题。
优化后的完整代码
import JavaScriptKit import TokamakDOM import CombineShim let global = JSObject.global let document = global.document /// 示例游戏状态,仅指定渲染到`<canvas>`元素的圆形的暗度。 class GameState: ObservableObject { @TokamakDOM.Published var circleGrayLevel = 0.0 } struct Canvas: View { /// 当前Canvas实例应渲染的状态。 @ObservedObject var gameState: GameState /// 获取Tokamak创建的`<canvas>`元素引用 @State private var canvas: JSObject! private func paint() { guard let ctx = canvas.getContext!("2d").object else { return } // 仅当状态变化时重绘(你可以在这里实现局部重绘逻辑) let v = (1 - gameState.circleGrayLevel) * 255 ctx.fillStyle = .string("rgb(\(v) \(v) \(v))") _ = ctx.clearRect(0, 0, canvas.width, canvas.height) _ = ctx.beginPath() _ = ctx.arc(100, 75, 50, 0, 2 * Double.pi); _ = ctx.fill() _ = ctx.stroke() } var body: some View { DynamicHTML( "canvas", // 仅初始化时设置尺寸,后续更新不会修改,避免画布清空 ["width": "\(300)", "height": "\(300)"], listeners: [:] ) ._domRef($canvas) .onAppear(perform: { // 设置画布样式 canvas.style.imageRendering = "pixelated" // 首次绘制 paint() }) // 监听状态变化,DOM更新完成后触发重绘 .onChange(of: gameState.circleGrayLevel) { _ in paint() } } } struct GameApp: App { @StateObject private var gameState = GameState() var body: some Scene { WindowGroup("Test") { VStack { Canvas(gameState: gameState) Slider(value: $gameState.circleGrayLevel, in: 0...1) } } } } GameApp.main()
关键改进点
- 移除手动订阅与
setTimeout:用.onChange(of: gameState.circleGrayLevel)替代手动订阅objectWillChange,这个修饰符会在状态值变更且视图完成DOM更新后执行回调,确保你访问的是最新的状态,无需延迟。 - 直接创建Canvas元素:不再需要嵌套div,直接用
DynamicHTML("canvas")创建画布元素,通过_domRef获取引用。将width/height作为静态属性传入DynamicHTML的属性字典,这样Tokamak只会在初始化时设置一次,后续更新不会修改这些属性,避免了画布缓冲区被清空的问题。 - 更贴合Tokamak的状态流:现在状态管理完全由Tokamak负责,你只需要关注状态变化时的重绘逻辑,不需要手动维护订阅生命周期,代码更简洁可靠。
局部重绘的支持
针对你提到的「每次更新仅需重绘部分元素」的需求,这个方案完全兼容。你可以在paint()函数中加入逻辑判断,比如记录上一次的状态变化范围,只重绘受影响的区域(比如特定的tiles),而不需要全量清空画布。Tokamak的状态更新只会触发paint()调用,具体重绘范围由你完全控制。
内容的提问来源于stack exchange,提问作者Feuermurmel
相关产品推荐
相关产品推荐

