gnperdue
(Gabriel Perdue)
1
I'd like to reuse some computed local values inside a GeometryReader that depend on the reader, e.g.
struct ContentView: View {
let verticalPaddingFraction: CGFloat = 0.05
let horizontalPaddingFraction: CGFloat = 0.05
var body: some View {
GeometryReader { reader in
// want to define corners here...
Path { p in
let bottomLeadingCorner = CGPoint(
x: reader.size.width * self.horizontalPaddingFraction,
y: reader.size.height * (1 - self.verticalPaddingFraction))
let bottomTrailingCorner = CGPoint(
x: reader.size.width * (1 - self.horizontalPaddingFraction),
y: reader.size.height * (1 - self.verticalPaddingFraction))
p.move(to: bottomLeadingCorner)
p.addLine(to: bottomTrailingCorner)
}
.stroke(Color.black, lineWidth: 5.0)
// now, would like a new Path with access to bottomLeadingCorner, etc.
}
}
}
I define corners inside the Path closure - how can I make them visible to another Path? Of course, if all I cared about was drawing from corner to corner I could just draw inside a rectangle. I'm interested in the general principle and chose corners here to make the example as simple and short as possible.
Thanks for any thoughts!
How about something like this:
struct ContentView: View {
let verticalPaddingFraction: CGFloat = 0.05
let horizontalPaddingFraction: CGFloat = 0.05
var body: some View {
GeometryReader { reader in
let bottomLeadingCorner = CGPoint(
x: reader.size.width * self.horizontalPaddingFraction,
y: reader.size.height * (1 - self.verticalPaddingFraction))
let bottomTrailingCorner = CGPoint(
x: reader.size.width * (1 - self.horizontalPaddingFraction),
y: reader.size.height * (1 - self.verticalPaddingFraction))
Path { p in
p.move(to: bottomLeadingCorner)
p.addLine(to: bottomTrailingCorner)
}
.stroke(Color.black, lineWidth: 5.0)
// now, would like a new Path with access to bottomLeadingCorner, etc.
}
}
}
gnperdue
(Gabriel Perdue)
3
Thanks for taking the time to offer a suggestion. Unfortunately, that code does not compile - you get the error messages: Closure containing a declaration cannot be used with function builder 'ViewBuilder'.
I've been Googling around with that error message, but people seem to mostly hit it when trying to declare a local variable in body, with the solution being to add a return statement to the "view code" (basically). But there is no analogous trick to pull inside a GeometryReader.
cukr
4
You can use the same solution, with the caveat that the compiler is too dumb to realize what type it's returning, so you have to write it explicitly (<AnyView> in my example)
struct ContentView: View {
let verticalPaddingFraction: CGFloat = 0.05
let horizontalPaddingFraction: CGFloat = 0.05
var body: some View {
GeometryReader<AnyView> { reader in
let bottomLeadingCorner = CGPoint(
x: reader.size.width * self.horizontalPaddingFraction,
y: reader.size.height * (1 - self.verticalPaddingFraction))
let bottomTrailingCorner = CGPoint(
x: reader.size.width * (1 - self.horizontalPaddingFraction),
y: reader.size.height * (1 - self.verticalPaddingFraction))
let topLeadingCorner = CGPoint(
x: reader.size.width * self.horizontalPaddingFraction,
y: reader.size.height * self.verticalPaddingFraction)
return AnyView(
Group {
Path { p in
p.move(to: bottomLeadingCorner)
p.addLine(to: bottomTrailingCorner)
}.stroke(Color.black, lineWidth: 5.0)
Path { p in
p.move(to: bottomLeadingCorner)
p.addLine(to: topLeadingCorner)
}.stroke(Color.black, lineWidth: 5.0)
Path { p in
p.move(to: topLeadingCorner)
p.addLine(to: bottomTrailingCorner)
}.stroke(Color.black, lineWidth: 5.0)
}
)
}
}
}
1 Like
gnperdue
(Gabriel Perdue)
5
Ah, yes, that works, thanks. Interesting! I often feel the compiler is smarter than me, so I'm hesitant to call it dumb... ;)
1 Like
cukr
6
Ah, maybe I should've used "not smart enough" instead of "too dumb" :) swift compiler often says "I'm not sure what you mean, could you clarify yourself?" and I like it (compared to alternatives)
1 Like
Lantua
7
Or, if you want to have compiler still be relatively smarter than you (or at least, than me), and not to rely on type erasure, which should be avoided for a lot of reasons. How about make it a function:
var body: some View {
GeometryReader(content: geometricView(with:))
}
func geometricView(with geometry: GeometryProxy) -> some View {
let buttomLeadingCorner = ...
let bottomTrailing = ...
return Group {
...
}
}
Refactoring like this also helps a lot with compilation.
4 Likes
gnperdue
(Gabriel Perdue)
8
If I could ask a follow up on this, would it be best practice to use a ZStack for multiple calls like this?, e.g.,
var body: some View {
ZStack {
GeometryReader(content: drawXYAxis(with:))
GeometryReader(content: drawXAxisTicks(with:))
GeometryReader(content: drawYAxisTicks(with:))
}
}
That appears to basically work, but I don't know if that is recommended.
Also, how would I pass data to one of these GeometryReaders? Looking at Apple Developer Documentation it looks like the content: argument takes a closure like (GeometryProxy) -> Content, but how do I sneak data in there too? Do I need to use the environment or some observable object business?
cukr
9
Is the data available at the point you create the closure? If so, you can capture your data just by using it inside the closure
struct ContentView: View {
var data = 123
var body: some View {
GeometryReader { proxy in
Text("Sneaky data: \(self.data)")
}
}
}
Lantua
10
You can have drawXYAxis use local variable, if you can compute it that early:
func drawXYAxis(with geometry: ...) {
let relatedData = self.relatedData
...
}
Or you can call the function manually, it'd still be single-statement closure, and so it'd still infer the return type properly
var body: some View {
let data = ...
...
GeometryReader { drawXYAxis(with: $0, data: data) }
}
func drawXYAxis(with geometry: ..., data: ...) { ... }
1 Like
young
(rtSwift)
11
Saw this tweet yesterday: Is relying on two GeometryReader's in the same SwiftUI view two too many? Uhh, asking for a friend.
One of the answer:
Try to avoid it as much as you can ;) Use shapes instead in case when you are drawing something. Shape provides you a coordinate space using Rect.
Not sure if it applies to your case, but you may find it helpful ...
1 Like
ingconti
(ingconti)
12
I did solve breaking down. I got expression too complex...
I DO precalc in a previous nested loop.
struct MyGridView : View {
var room: Room
let MaxRows = 2
let MaxCols = 2
var body: some View {
**// precalc, as GeometryReader calls its body loop twice**
let si = FirstLevelQuestionsManager.shared
var firstLevelQuestions = FirstLevelQuestions()
var troubleShootings = TroubleShootings()
let devids = room.devids
for row in 0..<self.MaxRows {
for col in 0..<self.MaxCols {
let flq = si.titleAndTextAt(row: row, col: col, ncols: self.MaxCols)
firstLevelQuestions.append(flq)
}
}
let gr = GeometryReader { (geometry : GeometryProxy) in
VStack() {
ForEach(0..<self.MaxRows) { (row: Int) in
HStack {
ForEach(0..<self.MaxCols) { (col: Int) -> GridCellView in
let index = col + row * self.MaxCols
return GridCellView(
w: (geometry.size.width / CGFloat(self.MaxCols))
titleAndText: firstLevelQuestions[index],
room: self.room,
troubleShootings: nil)
}
}
}
}
} // GeometryReader
return gr
}
}