swiftswiftui

Expand parent view to encompass children with offset


I'm making a view to represent certain fractions and I need the parent to adjust its size to the children inside.

I have a Text view and Canvas (drawing the square root symbol) in a ZStack.

When I offset the ZStack to position the square root better, the parent doesn't adjust accordingly. I know that offset on a child moves the view visually but doesn't affect the parent's layout, so whats the appropriate way to do this?

I'd like to figure out how to position the square root, and have the green border encompass all the bounding area, because later on I'm adding a fraction bar, and I want it to be the width of the view.

struct CoordinateView: View {
  var body: some View {
    VStack(alignment: .leading) {
      TermView(value: 3, isSquareRoot: true)
    }
    .border(.green)
  }
}

struct TermView: View {
  var value: Int
  var isSquareRoot: Bool
  
  var body: some View {
    ZStack {
      Group {
        if isSquareRoot {
          SquareRootOverlay()
        }
      }
      Text("\(value)")
        .fontDesign(.rounded)
        .font(.system(size: 36))
        .fontWeight(.bold)
    }
  }
}

struct SquareRootOverlay: View {
  var body: some View {
    Canvas { context, size in
      let path = Path { path in
        path.move(to: CGPoint(x: 4, y: 18))
        path.addLine(to: CGPoint(x: 14, y: 38))
        path.addLine(to: CGPoint(x: 25, y: 4))
        path.addLine(to: CGPoint(x: 57, y: 4))
      }
      context.stroke(path, with: .color(.black), style: StrokeStyle(lineWidth: 4.4, lineCap: .round, lineJoin: .round))
    }
    .frame(width: 62, height: 44)
    .background(.yellow)
    .offset(x: -5, y: -3)
  }
}

Parent resizes to fit 3 Parent resizes to fit canvas Parent does not resize to fit offset


Solution

  • Try showing SquareRootOverlay using either .overlay or .background. This way, the frame size is determined by the size of the view it is applied to.

    Since SquareRootOverlay currently has a yellow background, you need to use .background and not .overlay.

    // TermView
    
    var body: some View {
        Text("\(value)")
            .fontDesign(.rounded)
            .font(.system(size: 36))
            .fontWeight(.bold)
            .background {
                if isSquareRoot {
                    SquareRootOverlay()
                }
            }
    }
    
    

    Screenshot

    Currently, you are setting a fixed-size frame on SquareRootOverlay. It would probably be a better idea to add padding to the base view (the number), then constrain the square-root sign to the bounds of the view it is being applied to. The Canvas receives the size of the underlying view, so there is no need for a hard-coded size. Something like:

    // TermView
    
    var body: some View {
        Text("\(value)")
            .fontDesign(.rounded)
            .font(.system(size: 36))
            .fontWeight(.bold)
            .padding(.top, isSquareRoot ? 4 : 0)
            .padding(.leading, isSquareRoot ? 30 : 0)
            .padding(.trailing, isSquareRoot ? 10 : 0)
            .background {
                if isSquareRoot {
                    SquareRootOverlay()
                }
            }
    
    struct SquareRootOverlay: View {
        var body: some View {
            Canvas { context, size in
                let path = Path { path in
                    path.move(to: CGPoint(x: 4, y: size.height - 29))
                    path.addLine(to: CGPoint(x: 14, y: size.height - 9))
                    path.addLine(to: CGPoint(x: 25, y: 4))
                    path.addLine(to: CGPoint(x: size.width - 4, y: 4))
                }
                context.stroke(
                    path,
                    with: .color(.black),
                    style: StrokeStyle(lineWidth: 4.4, lineCap: .round, lineJoin: .round)
                )
            }
            .background(.yellow)
        }
    }
    

    Screenshot