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 Views 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() }
1 Answer 1
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 @States. 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
}
}
2 Comments
GeometryReader set up, just seems silly when Canvas already does it exactly down to the last pixel. I said what you said "they will necessarily have the same size in the above code" - so why double-overhead it for the numbers that Canvas always has in-hand? The DispatchQueue is also a bit "thread"y of a solution, but clever in this spy game. Observable upgrades my tiny struct into class class. But the onGeometryChange just might work and stay small? Will try, thank you.of function in .onGeometryChange. This certainly helps to keep the code more compact (+1).Explore related questions
See similar questions with these tags.
sizeyou get in aCanvasshould be the same as the size you get from theGeometryProxyif you used aGeometryReaderinstead. Why not just wrap aGeometryReaderaround aCanvas?GeometryReaderhas to offer (has to calculate), when the exact answer is already provided right there in front of your face? It seems utterly pointless, like using 2GeometryReaders.Canvasis already doing it perfectly, every time... just don't know how to pass the silly number one line over the wall (outside of the closure bracket).Canvasfor a laugh. Felt like I was playing a secret agent/spy game where I try to find clever ways to get a secret message out. Silly...@Observableclass that wraps aCGSize, but I'd imagine that would be even more overhead. TheCanvasclosure gets called during a view update, and you cannot set any@States during a view update. You can set it after the view update with aDispatchQueue.main.asyncAfter, but IMO aGeometryReaderis much cleaner.