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