Search code examples
swiftswiftuigraphics2drenderer

Recursive Rendering in SwiftUI


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?


Solution

  • 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()
                }
            }
        }
    }