在某些应用程序中,程序员希望写入持久图像或视图。这将避免在视图更新时重新渲染旧数据的需要。也就是说,视图的更新理想情况下只需要渲染新路径,而不需要浪费地重新渲染所有以前的(未更新的)路径。我将这种功能称为“递归渲染”——这意味着可以通过编程方式更改显示的图像,而无需浪费时间将旧数据重新绘制到屏幕上。
为了回答我在 StackOverflow 上提出的关于如何做到这一点的第一个问题,@rob mayoff 提出了一个创新的解决方案,包括 (1) 绘制到 Canvas 中,(2) 将 Canvas 转换为图像(显示在屏幕上),以及( 3) 将图像绘制回画布,然后再次在其上书写。 (参见here。)这个反馈循环满足我的要求,但它会产生模糊结果并且存在内存泄漏。
我修改了 Rob 的答案,并提出解决它的问题作为我的第二个 StackOverflow 问题。 (参见here。)一位Apple开发者技术支持工程师告诉我,问题(模糊线条和内存泄漏)是在Canvas和Image之间来回切换所固有的,并提出了一些干净的代码来解决这些问题。不幸的是,代码没有进行递归渲染。它仍然需要为每次数据更新渲染所有旧路径(即使是不受更新影响的路径)。
我仍在寻找一种解决方案来满足我对持久 SwiftUI 视图/图像/画布的渴望,我可以不断地将新数据绘制到其中,而不必重新渲染我仍然希望在屏幕上可见的旧数据。
我进一步探索了这个想法,最终意识到我可以创建一个可变且持久的Core Graphics CGContext。下面打印的是一个完整的 Xcode 项目,其中: (1) DataGenerator ObservedObject 类每十分之一秒发布一个 4 元素随机数数组; (2) ContentView 结构接收这个观察到的数组并将其传递给 ViewModel 类; (3) ViewModel 类声明一个持久的 CGContext 并在其上绘制一条随机线(端点由接收到的 4 元素数组指定);和 (4) 创建CGContext的图像并将其发布到ContentView进行显示。 此过程一遍又一遍地重复(使用相同的持久 CGContext),显示绘制线条的累积历史记录,而无需将任何旧数据重新渲染到屏幕上。
为了美观,ViewModel 会定期在渲染白线和黑线之间切换。
我对这个解决方案感到非常自豪,但我更愿意使用所有现代 SwiftUI 工具,而不是旧的 Core Graphics 工具。谁能告诉我如何使用 SwiftUI Canvas (它创建一个 GraphicsContext - 类似于 CGContext)来做到这一点?看来 SwiftUI 不允许我简单地声明 GraphicsContext (正如我在下面声明的 CGContext 一样)。 GraphicsContext 仅由 Canvas 创建,并且仅在该 Canvas 的闭包内有效。我无法使用 SwiftUI 的 Canvas 视图编写类似于下面的应用程序。
import SwiftUI
@main
struct RecursiveDrawingApp: App {
@StateObject var dataGenerator = DataGenerator()
@StateObject var viewModel = ViewModel()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(dataGenerator)
.environmentObject(viewModel)
}
}
}
final class DataGenerator: ObservableObject {
@Published var myArray: [Double] = [Double](repeating: 0.0, count: 4)
init() {
Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { _ in
for x in 0 ..< self.myArray.count {
self.myArray[x] = Double.random(in: 0.0...1.0)
}
}
}
}
struct ContentView: View {
@EnvironmentObject var dataGenerator: DataGenerator
@EnvironmentObject var viewModel: ViewModel
@Environment(\.displayScale) var displayScale: CGFloat
var body: some View {
ZStack {
if viewModel.cgImage != nil {
Image(decorative: viewModel.cgImage!,
scale: displayScale,
orientation: .up
)
.resizable()
}
}
.onReceive(dataGenerator.$myArray) { value in // onReceive subscribes to the "dataGenerator" publisher.
viewModel.generateImages( withData: value )
}
}
}
class ViewModel: ObservableObject {
@Published var cgImage: CGImage?
static let width: Int = 1_000
static let height: Int = 1_000
static var counter: Int = 0
static let context = CGContext(data: nil,
width: width,
height: height,
bitsPerComponent: 8,
bytesPerRow: width * 4,
space: CGColorSpace(name: CGColorSpace.sRGB)!,
bitmapInfo: CGImageAlphaInfo.noneSkipLast.rawValue )
let width = Double( ViewModel.width )
let height = Double( ViewModel.height )
func generateImages( withData data: [Double] ) {
ViewModel.context?.move( to: CGPoint( x: data[0] * width, y: data[1] * height ) )
ViewModel.context?.addLine(to: CGPoint( x: data[2] * width, y: data[3] * height ) )
ViewModel.context?.setLineWidth(1)
ViewModel.counter = ViewModel.counter >= 1200 ? 0 : ViewModel.counter + 1
ViewModel.context?.setStrokeColor( ViewModel.counter < 600 ?
CGColor(red: 255, green: 255, blue: 255, alpha: 1.0) : // white
CGColor(red: 0, green: 0, blue: 0, alpha: 1.0) ) // black
ViewModel.context?.strokePath()
cgImage = ViewModel.context?.makeImage()
}
}