2

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() }
asked Apr 13, 2025 at 5:59
4
  • The size you get in a Canvas should be the same as the size you get from the GeometryProxy if you used a GeometryReader instead. Why not just wrap a GeometryReader around a Canvas? Commented Apr 13, 2025 at 6:00
  • I did... I do... I don't want to. Why have the overhead of all that GeometryReader has 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 2 GeometryReaders. Canvas is 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). Commented Apr 13, 2025 at 6:11
  • I actually drew it (the size) once on the screen in the Canvas for 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... Commented Apr 13, 2025 at 6:18
  • You can create an @Observable class that wraps a CGSize, but I'd imagine that would be even more overhead. The Canvas closure 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 a DispatchQueue.main.asyncAfter, but IMO a GeometryReader is much cleaner. Commented Apr 13, 2025 at 6:22

1 Answer 1

3

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
 }
}
answered Apr 13, 2025 at 6:14
Sign up to request clarification or add additional context in comments.

2 Comments

All keen choices. I already had the 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.
I didn't know you can use the path to a property as the of function in .onGeometryChange. This certainly helps to keep the code more compact (+1).

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.