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