How to define local variables inside a `GeometryReader`

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.
       }
    }
}

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.

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

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

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

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

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?

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

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

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

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
    
}

}