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