I need the 'size' value provided by Canvas
' GraphicsContext
closure, outside of the closure, but no matter what I do, whatever happens in Vegas... I mean whatever happens in Canvas
, stays in Canvas
- I can't mutate (?) external values no matter what I've tried. I can read/use all external values, I just can’t change/write to any of them. Here is a sample (and, yes, my .frame() is deciding the size in the example, but the View
is destined to be a subView in other View
s where the user or UI decides the actual/final size):
import SwiftUI
struct ContentView3: View {
@State var var1 = 0.25
@State var var2 = 0.75
@State var someStr = "Hey"
var someVar = 0.5
var body: some View {
Canvas { context, size in
var1 = size.width //var1 never changes.
var2 += 37 //var2 never changes.
someStr = String("\(size.width)") //someStr never changes.
//the value of someVar is allowed to pass in.
context.fill(Ellipse().path(in: CGRect(origin: .zero, size: CGSize(width: someVar*size.width, height: size.height))), with: .color(Color.blue))
}
.frame(width: 400, height: 300)
//The values never changed:
HStack {
Spacer()
Text("var1: \(var1)")
Text(someStr)
Text("var2: \(var2)")
Spacer()
}
}
}
#Preview { ContentView3() }
The CGSize
parameter of the Canvas
closure is just the size of the Canvas
. If you want to store that in a @State
, you can separately use onGeometryChange
to get the size of the Canvas
:
@State var size = CGSize.zero
var body: some View {
Canvas { gc, size in
// ...
}
.onGeometryChange(for: CGSize.self, of: \.size) { newValue in
size = newValue
}
}
If you need to support older OS versions, you can wrap a GeometryReader
like this too:
@State var size = CGSize.zero
var body: some View {
GeometryReader { geo in
Canvas { gc, size in
// ...
}
.onAppear { size = geo.size }
.onChange(of: geo.size) { size = $0 }
}
}
Since GeometryReader
and Canvas
both honour size proposals (aka "taking up all available space"), they will necessarily have the same size in the above code.
The Canvas
closure is called during a view update. This is when you cannot modify any @State
s. If you delay the line that sets the @State
so that it runs after the view update has completed, it works:
@State var size = CGSize.zero
var body: some View {
Canvas { gc, size in
DispatchQueue.main.async {
self.size = size
}
}
}
Instead of updating a @State
, you can also set a property in an @Observable
class. This is allowed even during a view update.
@Observable
class SizeWrapper {
var size = CGSize.zero
}
@State var sizeWrapper = SizeWrapper()
var body: some View {
Canvas { gc, size in
sizeWrapper.size = size
}
}