我想在我的 SwiftUI 应用程序中完成递归渲染。当渲染非常大的数据数组时,这是理想的,并且在每次视图更新期间只需要更新其中的一小部分。我之前在here问过类似的问题,@Rob Mayoff 向我指出了 SwiftUI 的
ImageRenderer
API。但他的代码使用了MVC设计模式,我想避免这种情况。我已经实现了一个可以运行的“最小可重现示例”应用程序(尽管它有两个问题,这是这个问题的焦点)。为简单起见,下面的代码仅适用于 macOS,但只需进行一些小的更改即可使其适用于 iOS 或多平台。
DataGenerator
类(使用协议ObservableObject
)每隔十分之一秒更新一个名为myVariable
的变量,并将其发布到所有视图。这个应用程序只包含一个名为 MyView
的视图,它观察 DataGenerator
的实例,因此每次 myVariable
发生变化时都会重绘。 MyView
使用 SwiftUI 的 myImage
命令声明一个名为 Image()
的静态变量。然后,我创建一个 Canvas 并通过将当前的 myImage
绘制到其中来初始化它。然后我添加一条水平线(垂直高度由 myVariable
确定)。然后,我使用 ImageRenderer()
命令将 Canvas 转换为名为 renderer
的位图图像数据,并使用 SwiftUI 的 renderer
命令渲染此 Image()
。
最后的语句用
myImage
位图图像数据更新原始renderer
,形成递归循环。
MyView.myImage = Image(nsImage: renderer .nsImage!)
不幸的是,这是非视图代码,如果我尝试将其放入我的
MyView
结构中,Xcode 会抱怨。所以我不得不采用@Andrew_STOP_RU_WAR_IN_UA建议的ExecuteCode
结构技巧here。
下面的代码是完整的 Swift 应用程序。它也在 GitHub here 上。您可以将其复制到 Xcode 中并运行它。但它有两个问题:
1.) 每次迭代,屏幕上渲染的图像都会逐渐模糊。新的水平线首次出现时清晰可见,但您可以看到它们在每次迭代中逐渐融入背景时变得更加模糊。在 SO Q&A here 中,@iOSDevil 遇到了一个相关问题,这表明我的问题是使用 NSImage 的图像表示来更新
myImage
,这是一种视图表示,因此使用“屏幕大小的图像”而不是使用底层 NSImage 的全分辨率进行更新。我不确定这是我的模糊问题的原因,但我不知道该怎么办。
2.) 应用程序存在内存泄漏。随着应用程序继续运行(窗口逐渐充满水平线),RAM 使用指示器不断向上,直到应用程序在系统内存耗尽时冻结。我推测
renderer
位图在用于更新 myImage
后没有被正确处理。在我向 Apple 提交错误报告之前,我想确保这次内存泄漏不是由于我的编程不当造成的。
如果有任何有关如何简单有效地实现递归渲染的建议,我将不胜感激。我还没有与 SwiftUI 结婚,所以,如果有更简单的方法,请告诉我。
import SwiftUI
@main
struct RecursiveRenderingApp: App {
var body: some Scene {
WindowGroup {
MyView()
.environmentObject(DataGenerator.generator)
}
}
}
class DataGenerator: ObservableObject {
static let generator = DataGenerator()
@Published var myVariable: Double = 0.0
init() {
Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { _ in
self.myVariable = Double.random(in: 0.0 ... 1.0)
}
}
}
@MainActor
struct MyView: View {
@EnvironmentObject var generator: DataGenerator
static let mySize = CGSize(width: 1_000, height: 1_000)
static var myImage = Image(size: mySize, opaque: true) { gc in
gc.fill(Path(CGRect(origin: .zero, size: mySize)), with: .color(.white))
}
var body: some View {
let canvas = Canvas { context, size in
context.draw(MyView.myImage, in: CGRect(origin: .zero, size: MyView.mySize))
var path = Path()
path.move( to: CGPoint( x: 0.0, y: generator.myVariable * size.height ) )
path.addLine(to: CGPoint( x: size.width, y: generator.myVariable * size.height ) )
context.stroke( path, with: .color( Color.black ), lineWidth: 1.0 )
}.frame(width: MyView.mySize.width, height: MyView.mySize.height)
let renderer = ImageRenderer(content: canvas)
Image(nsImage: renderer.nsImage!)
.resizable()
.aspectRatio(contentMode: .fill)
ExecuteCode {
MyView.myImage = Image(nsImage: renderer.nsImage!)
}
}
}
// Use this struct to insert non-View code into a View. (Use with caution.):
struct ExecuteCode : View {
init( _ codeToExec: () -> () ) {
codeToExec()
}
var body: some View {
EmptyView()
}
}
我将上述代码提交给Apple开发者技术支持以寻求问题的解决方案。一位苹果工程师回应说,我的方法不可避免地会导致每次导出和重新渲染图像数据时出现内存峰值和有损压缩。它不可避免地会产生模糊线和内存泄漏。他提供了大大改进的代码,重印如下。
好消息是他的代码是非常干净、编写良好的多平台 Swift 代码。它产生清晰(非模糊)的线条,并且没有内存泄漏。
坏消息是它不是我所定义的“递归渲染”。它为每个 myVariable 更新绘制所有路径 - 包括重新绘制之前更新中未更改的路径。
在我看来,这仍然是极其低效的。尽管如此,我还是在下面打印了他的解决方案,以帮助其他可能在其特定应用程序中从中受益的 SO 用户。我仍在寻找一种有效的解决方案,该解决方案要求我仅渲染一次路径并将它们保留在我的屏幕窗口中。
import SwiftUI
@main
struct RecursiveRenderingApp: App {
@StateObject var generator = DataGenerator()
var body: some Scene {
WindowGroup {
MyView()
.environmentObject(generator)
}
}
}
class DataGenerator: ObservableObject {
@Published var myVariable: Double = 0.0
init() {
Timer.publish(every: 0.1, on: .main, in: .common)
.autoconnect()
.map { _ in Double.random(in: 0.0...1.0) }
.assign(to: &$myVariable)
}
}
struct MyView: View {
@EnvironmentObject var generator: DataGenerator
@StateObject private var renderer = ImageRenderer(content: LineCanvas(values: []))
var body: some View {
LineCanvasImage(renderer: renderer)
.onReceive(generator.$myVariable) { value in
renderer.content.values.append(value)
}
}
}
struct LineCanvasImage: View {
@ObservedObject var renderer: ImageRenderer<LineCanvas>
var body: some View {
if let cgImage = renderer.cgImage {
Image(decorative: cgImage, scale: renderer.scale)
.frame(width: LineCanvas.size.width, height: LineCanvas.size.height)
}
}
}
@MainActor
struct LineCanvas: View {
static let size = CGSize(width: 1_000, height: 1_000)
var values: [Double]
var body: some View {
Canvas { context, size in
for value in values {
var path = Path()
path.move(to: CGPoint(x: 0, y: value * size.height))
path.addLine(to: CGPoint(x: size.width, y: value * size.height))
context.stroke(path, with: .color(.black), lineWidth: 1)
}
}
.frame(width: Self.size.width, height: Self.size.height)
}
}
经过大量的反复试验,我想出了一种使用持久 CGContext 执行递归渲染的方法。描述和代码显示于here。我仍然希望有人能向我展示如何使用持久视图、图像、画布或 GraphicsContext 在 SwftUI 中完成此任务。