Canvas 不会在 SwiftUI 中重绘

发布于 2025-01-14 12:44:15 字数 2667 浏览 1 评论 0原文

我在 macOS 上的 SwiftUI 中有一个项目,我每秒在画布上绘制两次。

这是我的 ContentView:

struct ContentView: view {
    @State var score: Int = 0

    var body: some View {
        VStack {
            Text("Score: \(self.score)")
                .fixedSize(horizontal: true, vertical: true)
            Canvas(renderer: { gc, size in
                start(
                    gc: &gc,
                    size: size
                    onPoint: { newScore in
                        self.score = newScore
                    }
                )
            )
        }
    }
}

start 函数:

var renderer: Renderer
func start(
    gc: inout GraphicsContext,
    size: size,
    onPoint: @escaping (Int) -> ()
) {
    if renderer != nil {
        renderer!.set(gc: &gc)
    } else {
        renderer = Renderer(
            context: &gc,
            canvasSize: size,
            onPoint: onPoint
        )
        startGameLoop(renderer: renderer!)
    }

    renderer!.drawFrame()
}

var timer: Timer
func startGameLoop(renderer: Renderer) {
    timer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true, block: {
         renderer!.handleNextFrame()
     }
}

渲染器大致如下所示:

class Renderer {
    var gc: GraphicsContext
    var size: CGSize
    var cellSize: CGFloat
    let pointCallback: (Int) -> ()
    var player: (Int, Int) = (0,0)

    init(
        context: inout GraphicsContext,
        canvasSize: CGSize,
        onPoint: (Int) -> ()
    ) {
        self.gc = gc
        self.size = canvasSize
        self.pointCallback = onPoint
        self.cellSize = min(self.size.width, self.size.height)
    }
}

extension Renderer {
    func handleNextFrame() {
        self.player = (self.player.0 + 1, self.player.1 + 1)

        self.drawFrame
    }

    func drawFrame() {
        self.gc.fill(
            Path(
                x: CGFloat(self.player.0) * self.cellSize,
                y: CGFloat(self.player.1) * self.cellSize,
                width: self.cellSize,
                height: self.cellSize
            )
        )
    }
}

因此,每秒调用 handleNextFrame 方法两次,该方法调用drawFrame 方法,将player 的位置绘制到画布上。

然而,画布上没有绘制任何东西。

仅绘制第一帧,它来自 start 中的 renderer!.drawFrame()。当得分时,画布也会重新绘制,因为 start 函数会再次被调用。

问题是,当从 handleNextFrame 调用 drawFrame 时,Canvas 上没有绘制任何内容。

我的问题出在哪里,我该如何解决这个问题?

预先感谢,
乔纳斯

I have a project in SwiftUI on macOS where I draw to a canvas twice per second.

This is my ContentView:

struct ContentView: view {
    @State var score: Int = 0

    var body: some View {
        VStack {
            Text("Score: \(self.score)")
                .fixedSize(horizontal: true, vertical: true)
            Canvas(renderer: { gc, size in
                start(
                    gc: &gc,
                    size: size
                    onPoint: { newScore in
                        self.score = newScore
                    }
                )
            )
        }
    }
}

The start function:

var renderer: Renderer
func start(
    gc: inout GraphicsContext,
    size: size,
    onPoint: @escaping (Int) -> ()
) {
    if renderer != nil {
        renderer!.set(gc: &gc)
    } else {
        renderer = Renderer(
            context: &gc,
            canvasSize: size,
            onPoint: onPoint
        )
        startGameLoop(renderer: renderer!)
    }

    renderer!.drawFrame()
}

var timer: Timer
func startGameLoop(renderer: Renderer) {
    timer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true, block: {
         renderer!.handleNextFrame()
     }
}

And the renderer roughly looks like this:

class Renderer {
    var gc: GraphicsContext
    var size: CGSize
    var cellSize: CGFloat
    let pointCallback: (Int) -> ()
    var player: (Int, Int) = (0,0)

    init(
        context: inout GraphicsContext,
        canvasSize: CGSize,
        onPoint: (Int) -> ()
    ) {
        self.gc = gc
        self.size = canvasSize
        self.pointCallback = onPoint
        self.cellSize = min(self.size.width, self.size.height)
    }
}

extension Renderer {
    func handleNextFrame() {
        self.player = (self.player.0 + 1, self.player.1 + 1)

        self.drawFrame
    }

    func drawFrame() {
        self.gc.fill(
            Path(
                x: CGFloat(self.player.0) * self.cellSize,
                y: CGFloat(self.player.1) * self.cellSize,
                width: self.cellSize,
                height: self.cellSize
            )
        )
    }
}

So the handleNextFrame method is called twice per second, which calls the drawFrame method, drawing the position of the player to the canvas.

However, there is nothing being drawn to the canvas.

Only the first frame is drawn, which comes from the renderer!.drawFrame() in start. When a point is scored, the canvas is also redrawn, because the start function gets called again.

The problem is that there is nothing being drawn to the Canvas when the drawFrame is called from handleNextFrame.

Where lies my problem, and how can I fix this issue?

Thanks in advance,
Jonas

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

滥情稳全场 2025-01-21 12:44:15

我有同样的问题。
转到项目设置,选择“构建设置”选项卡,并确保“严格并发检查”处于打开状态。
它会提示您 GraphicsContext 不是线程安全的,因为它不符合 Sendable 协议。

我不是该主题的专家,但我相信这意味着 GraphicsContext 不适合在异步上下文中使用,您不适合保存其引用以供将来使用。您将获得一个 GraphicsContext 实例,并且只要渲染器闭包的执行持续,您就应该使用它(当然,此执行会在异步回调可以使用 GraphicsContext 实例之前结束)。

您真正想要的是 TimelineView:

    import SwiftUI

private func nanosValue(for date: Date) -> Int { return Calendar.current.component(.nanosecond, from: date)
   }

struct ContentView: View {
    var body: some View {
        TimelineView(.periodic(from: Date(), by: 0.000001)) { timeContext in
            Canvas { context, size in
                let value = nanosValue(for: timeContext.date) % 2
                
                let rect = CGRect(origin: .zero, size: size).insetBy(dx: 25, dy: 25)

                           // Path
                           let path = Path(roundedRect: rect, cornerRadius: 35.0)

                           // Gradient
                let gradient = Gradient(colors: [.green, value == 1 ? .blue : .red])
                           let from = rect.origin
                           let to = CGPoint(x: rect.width + from.x, y: rect.height + from.y)
                           
                           // Stroke path
                           context.stroke(path, with: .color(.blue), lineWidth: 25)
                           
                           // Fill path
                           context.fill(path, with: .linearGradient(gradient,
                                                                    startPoint: from,
                                                                    endPoint: to))
            }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

这将每纳秒更改绘制矩形的渐变颜色。
有趣的是,我在一些博客上听说,你需要一个完全不同的心态来使用 SwiftUI,哈哈。它只是不能以传统方式工作。

I had the same issue.
Go to your project settings, select 'Build Settings' tab, and make sure 'Strict Concurrency Checking' is ON.
It will give you hints about GraphicsContext not being thread-safe, because it does not conform to Sendable protocol.

I am no expert on the topic, but I believe this means that GraphicsContext is not meant to be used in asynchronous context, you are not meant to save its reference for future use. You are given a GraphicsContext instance and you are meant to use it so long as the renderer closure's execution lasts (and this execution, of course, ends before your asynchronous callbacks could use the GraphicsContext instance).

What you really want is a TimelineView:

    import SwiftUI

private func nanosValue(for date: Date) -> Int { return Calendar.current.component(.nanosecond, from: date)
   }

struct ContentView: View {
    var body: some View {
        TimelineView(.periodic(from: Date(), by: 0.000001)) { timeContext in
            Canvas { context, size in
                let value = nanosValue(for: timeContext.date) % 2
                
                let rect = CGRect(origin: .zero, size: size).insetBy(dx: 25, dy: 25)

                           // Path
                           let path = Path(roundedRect: rect, cornerRadius: 35.0)

                           // Gradient
                let gradient = Gradient(colors: [.green, value == 1 ? .blue : .red])
                           let from = rect.origin
                           let to = CGPoint(x: rect.width + from.x, y: rect.height + from.y)
                           
                           // Stroke path
                           context.stroke(path, with: .color(.blue), lineWidth: 25)
                           
                           // Fill path
                           context.fill(path, with: .linearGradient(gradient,
                                                                    startPoint: from,
                                                                    endPoint: to))
            }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

This will change the gradient colour of the drawn rectangle every nanosecond.
Funny thing, I have heard on some blog, you need a completely different mindset to use SwiftUI, haha. It just does not work the traditional way.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文