使用持久的 SwiftUI 视图/图像/画布进行递归渲染

问题描述 投票:0回答:1

在某些应用程序中,程序员希望写入持久图像或视图。这将避免在视图更新时重新渲染旧数据的需要。也就是说,视图的更新理想情况下只需要渲染新路径,而不需要浪费地重新渲染所有以前的(未更新的)路径。我将这种功能称为“递归渲染”——这意味着可以通过编程方式更改显示的图像,而无需浪费时间将旧数据重新绘制到屏幕上。

为了回答我在 StackOverflow 上提出的关于如何做到这一点的第一个问题,@rob mayoff 提出了一个创新的解决方案,包括 (1) 绘制到 Canvas 中,(2) 将 Canvas 转换为图像(显示在屏幕上),以及( 3) 将图像绘制回画布,然后再次在其上书写。 (参见here。)这个反馈循环满足我的要求,但它会产生模糊结果并且存在内存泄漏。

我修改了 Rob 的答案,并提出解决它的问题作为我的第二个 StackOverflow 问题。 (参见here。)一位Apple开发者技术支持工程师告诉我,问题(模糊线条和内存泄漏)是在Canvas和Image之间来回切换所固有的,并提出了一些干净的代码来解决这些问题。不幸的是,代码没有进行递归渲染。它仍然需要为每次数据更新渲染所有旧路径(即使是不受更新影响的路径)。

我仍在寻找一种解决方案来满足我对持久 SwiftUI 视图/图像/画布的渴望,我可以不断地将新数据绘制到其中,而不必重新渲染我仍然希望在屏幕上可见的旧数据。

image canvas swiftui view cgcontext
1个回答
0
投票

我进一步探索了这个想法,最终意识到我可以创建一个可变且持久的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()
    }
}
© www.soinside.com 2019 - 2024. All rights reserved.