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

huangapple go评论97阅读模式
英文:

Recursive Rendering using a persistent SwiftUI View/Image/Canvas

问题

有一些应用程序,程序员希望有一个持久的图像或视图来进行写入。这将消除在视图更新时重新呈现旧数据的需要。也就是说,视图的更新理想情况下只需要呈现新的路径,而不需要浪费资源重新呈现所有先前(未更新的)路径。我将这种能力称为“递归渲染” - 这意味着可以以编程方式更改显示的图像,而无需浪费资源重新绘制旧数据到屏幕上。

在回应我的第一个StackOverflow问题,@rob mayoff提出了一个创新性的解决方案,涉及(1)在Canvas中绘制,(2)将Canvas转换为图像(显示在屏幕上),以及(3)在再次写入之前将图像绘制回Canvas。(请参见此处。)这个反馈循环满足了我的需求,但会产生模糊的结果并且存在内存泄漏问题。

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

我仍然在寻求一个解决方案,以实现我对一个持久的SwiftUI View/Image/Canvas的渴望,可以不断地在其中绘制新数据,而无需重新呈现我仍然希望在屏幕上看到的旧数据。

英文:

There are some applications where a programmer desires a persistent image or view to write to. This would obviate the need to re-render old data when a view gets updated. That is, the view's updates would ideally need to render only the new paths, and would not need to wastefully re-render all previous (non-updated) paths. I call this capability "Recursive Rendering" - meaning that the image displayed can be programmatically changed without the wastefulness of re-drawing old data to the screen.

In response to my first StackOverflow question on how to do this, @rob mayoff proposed an innovative solution involving (1) drawing into a Canvas, (2) converting the Canvas to an image (which gets displayed on the screen), and (3) drawing the image back into the Canvas before writing on it again. (See here.) This feedback loop satisfies my requirement, but it produces fuzzy results and has memory leaks.

I reworked Rob's answer and posed fixing it's problems as my second StackOverflow question. (See here.) An Apple Developer Technical Support engineer told me that the problems (fuzzy lines & memory leaks) were inherent in switching back and forth between a Canvas and an Image, and proposed some clean code which fixed the problems. Unfortunately, the code did NOT do Recursive Rendering. It still required rendering all old paths for each data-update (even the paths not effected by the update).

I am still seeking a solution to my desire for a persistent SwiftUI View/Image/Canvas that I can continuously draw new data into without having to re-render old data that I still want visible on the screen.

答案1

得分: 0

我已经进一步探索了这个想法,并最终意识到我可以创建一个可变和持久的Core Graphics CGContext。以下是一个完整的Xcode项目,其中包括:

(1) 一个DataGenerator ObservableObject类,每隔十分之一秒发布一个包含4个随机数字的数组;
(2) 一个ContentView结构,在其视图中显示一幅图像,并将观察到的数组传递给DrawingPane结构;
(3) DrawingPane结构声明一个CGContext,并在其上绘制一条随机线条(端点由接收到的4个元素数组指定);
(4) 它创建CGContext的图像并将其返回给ContentView以进行显示。这个过程一次又一次地重复进行(使用相同的持久CGContext),而不必重新渲染任何旧数据到屏幕上。

我对这个解决方案感到非常自豪,但我更希望使用所有现代的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()
    var body: some Scene {
        WindowGroup {
            ContentView()
                .environmentObject(dataGenerator)
        }
    }
}

final class DataGenerator: ObservableObject {
    @Published var myArray = [Double](repeating: 0.0, count: 4)
    var tempArray = [Double](repeating: 0.0, count: 4)
    init() {
        Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { _ in
            for x in 0 ..< 4 {
                self.tempArray[x] = Double.random(in: 0.0 ... 1.0)
            }
            self.myArray = self.tempArray
        }
    }
}

struct ContentView: View {
    @EnvironmentObject var dataGenerator: DataGenerator
    @State var drawingPane = DrawingPane()
    @State var myCGImage: CGImage?
    @State var counter: Int = 0
    @State var myColor: CGColor = .white
    @Environment(\.displayScale) var displayScale: CGFloat
    
    var body: some View {
        ZStack {
            if myCGImage != nil {
                Image(decorative: myCGImage!, scale: displayScale, orientation: .up).resizable()
            }
        }
        .onReceive(dataGenerator.$myArray) { value in
            counter = counter < 400 ? counter + 1 : 0
            myColor = counter < 200 ? .white : .black
            myCGImage = drawingPane.addLine(data: value, color: myColor)
        }
    }
}

struct DrawingPane {
    static let width: Int = 1_000
    static let height: Int = 1_000

    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(DrawingPane.width)
    let height = Double(DrawingPane.height)

    func addLine(data: [Double], color: CGColor) -> CGImage {
        context?.move(to: CGPoint(x: data[0] * width, y: data[1] * height))
        context?.addLine(to: CGPoint(x: data[2] * width, y: data[3] * height))
        context?.setLineWidth(1)
        context?.setStrokeColor(color)
        context?.strokePath()
        return (context?.makeImage())!
    }
}
英文:

I have explored this idea further, and finally realized that I can create a Core Graphics CGContext that is mutable and persistent. Printed below is a complete Xcode project in which: (1) a DataGenerator ObservableObject class publishes a 4-element array of random numbers every tenth-of-a-second; (2) a ContentView struct displays an image in it's View, and passes this observed array to a DrawingPane struct; (3) the DrawingPane struct declares a CGContext and draws a random line onto it (with endpoints specified by the received 4-element array); and (4) it creates an image of the CGContext and returns it to the ContentView for display. This process repeats over-and-over (using the same persistent CGContext) without ever having to re-render any old data to the screen.

I am very proud of this solution, but I would much prefer to use all of the modern SwiftUI tools than the old Core Graphics tools. Can anyone show me how to do this with a SwiftUI Canvas (which creates a GraphicsContext - which is similar to a CGContext)? It appears that SwiftUI won't allow me to simply declare a GraphicsContext (as I declared a CGContext below). A GraphicsContext is only created by a Canvas and is only valid within the closure of that Canvas. I have been unable to code an app similar to the one below using SwiftUI's Canvas view.

import SwiftUI
@main
struct RecursiveDrawingApp: App {
@StateObject var dataGenerator = DataGenerator()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(dataGenerator)
}
}
}
final class DataGenerator: ObservableObject {
@Published var myArray = [Double](repeating: 0.0, count: 4)
var tempArray = [Double](repeating: 0.0, count: 4)
init() {
Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { _ in
for x in 0 ..&lt; 4 {
self.tempArray[x] = Double.random(in: 0.0 ... 1.0)
}
self.myArray = self.tempArray
}
}
}
struct ContentView: View {
@EnvironmentObject var dataGenerator: DataGenerator
@State var drawingPane = DrawingPane()
@State var myCGImage: CGImage?
@State var counter: Int = 0
@State var myColor: CGColor = .white
@Environment(\.displayScale) var displayScale: CGFloat
var body: some View {
ZStack {
if myCGImage != nil {
Image(decorative: myCGImage!, scale: displayScale, orientation: .up).resizable()
}
}
.onReceive(dataGenerator.$myArray) { value in       // onReceive subscribes to the &quot;dataGenerator&quot; publisher.
counter = counter &lt; 400 ? counter + 1 : 0
myColor = counter &lt; 200 ? .white : .black
myCGImage = drawingPane.addLine( data: value, color: myColor )
}
}
}
struct DrawingPane {
static let width:  Int = 1_000
static let height: Int = 1_000
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( DrawingPane.width )
let height = Double( DrawingPane.height )
func addLine( data: [Double], color: CGColor ) -&gt; CGImage {
context?.move(   to: CGPoint( x: data[0] * width, y: data[1] * height ) )
context?.addLine(to: CGPoint( x: data[2] * width, y: data[3] * height ) )
context?.setLineWidth(1)
context?.setStrokeColor(color)
context?.strokePath()
return (context?.makeImage())!
}
}

huangapple
  • 本文由 发表于 2023年8月4日 02:06:54
  • 转载请务必保留本文链接:https://go.coder-hub.com/76830624.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定