-1

My app draws dynamic complex graphics into SwiftUI Views. I understand that SwiftUI redraws Views when an observed variable changes, and that re-drawing a View deletes the existing View.

What I would like to accomplish is that the redraw should "add" (or write-on-top-of) a render to the View without clearing the existing rendered data. Is there a way to get SwiftUI to re-draw my View in such a way that it does not clear the existing graphics first?

KeithB
  • 417
  • 2
  • 12
  • Watch "Demystify SwiftUI" and the new WWDC videos from today. `ObservableObject`s are the least efficient but the new iOS17+ wrappers are supposed to outperform everything available iOS 13-16. It depends on your timeline and what you want to support. Also, depending on your setup `drawingGroup` or `compostingGroup` can help you shift the load to the GPU. – lorem ipsum Jun 06 '23 at 21:05

1 Answers1

1

In SwiftUI, the user interface is a function of your data model. I mean that literally. Each View has a body property which is essentially a function that takes one parameter, self, and returns a description of what to display, and how to respond to events, based on that self parameter. The self parameter has properties which are those parts of the data model needed by the body property to compute that description.

So, if your view would be particularly expensive to draw completely from scratch each time, you need to store the rendered appearance of your view as an image in your data model. Here’s a simple example:

import SwiftUI

@MainActor
struct Model {
    /// The completely rendered image to display.
    var image = Image(size: size, opaque: true) { gc in
        gc.fill(Path(CGRect(origin: .zero, size: size)), with: .color(.white))
    }

    static var size: CGSize { .init(width: 300, height: 300) }
    
    static func randomPoint() -> CGPoint {
        return .init(
            x: CGFloat.random(in: 0 ... size.width),
            y: CGFloat.random(in: 0 ... size.height)
        )
    }
    
    /// Update `image` by drawing a random line on top of its existing content.
    mutating func addRandomLine() {
        let oldImage = image
        let canvas = Canvas { gc, size in
            gc.draw(oldImage, in: CGRect(origin: .zero, size: Self.size))
            let path = Path {
                $0.move(to: Self.randomPoint())
                $0.addLine(to: Self.randomPoint())
            }
            gc.stroke(path, with: .color(.black), lineWidth: 1)
        }.frame(width: Self.size.width, height: Self.size.height)
        let uiImage = ImageRenderer(content: canvas).uiImage!
        image = Image(uiImage: uiImage)
    }
}

@MainActor
struct ContentView: View {
    @State var model = Model()
    
    var body: some View {
        VStack {
            model.image
                .frame(width: Model.size.width, height: Model.size.height)
            Button("Add Line") {
                model.addRandomLine()
            }
        }
    }
}
rob mayoff
  • 375,296
  • 67
  • 796
  • 848
  • Rob, thank you for your answer. I think that you have solved my problem. Let me play around with your code in my particular app for a while before I officially accept your answer. Again, thanks. – KeithB Jun 07 '23 at 15:28
  • Rob, thank you for introducing me to the ImageRenderer() API. How would I addRandomLine() whenever the observed variable "spectrum" is updated? Changing the ContentView to var body: some View { model.image.frame(width: Model.size.width, height: Model.size.height) model.addRandomLine(spectrum) } produces both a "Type cannot conform to View" error and a "Modifying state during view update will cause undefined behavior" error. – KeithB Jun 08 '23 at 18:30
  • I don’t know anything about a variable named `spectrum`. – rob mayoff Jun 08 '23 at 21:00
  • Rob, I just picked the name spectrum to be any variable published by an Observable Object class in my code. Whenever that variable gets updated, SwiftUI redraws any Views (such as your ContentView) that use it. I just need you to show me the syntax for adding a RandomLine to my Image() whenever ContentView is notified that the variable has been changed. I can handle making the addRandomLine() func depend on the variable. – KeithB Jun 08 '23 at 21:43
  • Whatever code updates `spectrum` should also call `addRandomLine`. It would not be appropriate to do it “whenever ContentView is notified that the variable has been changed.” – rob mayoff Jun 09 '23 at 09:25
  • Rob, thanks again for pointing me to the ImageRenderer() API. While experimenting with your code, I found a problem: If I increase the image size to say 2000 by 1000, and I trigger the addRandomLine() function by a 0.01-second Timer, then my system memory usage continuously rises until the app crashes. I suspect that the uiImage created by the ImageRender() command is not being released from memory after the app has used it to update the original image. Can you confirm this behavior? Is there a simple way for me to tell your code to release the uiImage from memory after usage? – KeithB Jun 21 '23 at 20:50
  • All I can suggest is that you run it under Instruments to look for leaks or to figure out where the allocations are happening. – rob mayoff Jun 21 '23 at 21:56
  • After considerable trial-and-error, I came up with a way to perform Recursive Rendering using a persistent CGContext. A description and code is shown [here](https://stackoverflow.com/questions/76830624/recursive-rendering-using-a-persistent-swiftui-view-image-canvas). I am still hoping that someone will show my how to accomplish this using a persistent View, Image, Canvas, or GraphicsContext. – KeithB Aug 20 '23 at 18:36