Search code examples
swiftuigeometryreaderlazyvgrid

How to reduce usage of geometry reader in a LazyVgrid?


In this code, is it a problem usage of geometryReader both in view and in cells? is there a different way to achieve same result?). to be clear if I have 5 cells, I had hard time keeping the image in the same position even if text is on single line. I need to avoid getting some cell with image close to top and other at a lower position

I'd like to keep these requirements:

  • the images should be at the same level in each cells, if the text is on a single line, I need the image is same "level" of cells where text is on two lines.

  • I'd like to avoid hard coded sizes, this way hopefully the views are more flexible

     import SwiftUI
    
     struct TestForBackgroundImage: View {
    
      @EnvironmentObject var classFromEntryPoint: ClassFromEntryPoint
      private static let minCommonWidth: CGFloat = 100
      let columns = [GridItem(.adaptive(minimum: minCommonWidth))]
    
      var body: some View {
              ScrollView {
                  LazyVGrid(columns: columns, spacing: 10) {
                      ForEach(0...100, id: \.self) { item in
                          ItemView(item: item)
                              .onTapGesture {
                                  print(item)
                              }
                      }
                  }
                  .padding()
          }
              .backgroundImage2()
    
      }
    
      private struct ItemView: View {
          let item: Int
    
          var body: some View {
              GeometryReader { geometry in
                  VStack(alignment: .center) {
                      Image(systemName: "apple.logo")
                      //                        Image(AppConstants.FixedImages.backgroundImage.rawValue)
                          .resizable()
                          .scaledToFill()
                          .foregroundStyle(.secondary)
                          .frame(width: geometry.size.width * 0.4, height: geometry.size.width * 0.4)
                      //this one is here for testing multiple text lenghts to be certain the image is always at the same level in each cell
                      if item % 2 == 0 {
                          Text("short: \(item)")
                              .frame(height: geometry.size.width * 0.25)
                              .lineLimit(2)
                              .minimumScaleFactor(0.8)
                              .multilineTextAlignment(.center)
                              .padding(.horizontal, 5)
                              .padding(.top, 5)
                      } else {
                          Text("middle lenght text: \(item)")
                              .frame(height: geometry.size.width * 0.25)
                              .lineLimit(2)
                              .minimumScaleFactor(0.8)
                              .multilineTextAlignment(.center)
                              .padding(.horizontal, 5)
                              .padding(.top, 5)
                      }
                  }
                  .frame(width: geometry.size.width, height: geometry.size.width)
                  .background(.teal)
                  .shadow(color: .black.opacity(0.3), radius: 10, x: 0, y: 10)
              }
              .aspectRatio(1, contentMode: .fit)
          }
      } 
     }
    
    
    
    
    
    
    
      struct BackgroundImage2: ViewModifier {
      func body(content: Content) -> some View {
          GeometryReader { geometry in
              content
                  .background(
                      Image(AppConstants.FixedImages.backgroundImage.rawValue)
                          .resizable()
                          .scaledToFill()
                          .opacity(0.4)
                          .offset(x: -geometry.size.width / 2, y:      geometry.size.height / 2)
                  )
          }
      }
     }
    
    extension View {
      func backgroundImage2() -> some View {
          modifier(BackgroundImage2())
      }
      }
    

Solution

  • If you don't need the 1:1 aspect ratio, you can just scaleToFit the image, and add a Spacer between the text and the image (or after the text, depending on what alignment you want):

    // ItemView's body:
    VStack {
        Image(systemName: "apple.logo")
            .resizable()
            .scaledToFit()
            .foregroundStyle(.secondary)
        Spacer()
        if item % 2 == 0 {
            Text("short: \(item)")
                .lineLimit(2)
                .minimumScaleFactor(0.8)
                .multilineTextAlignment(.center)
                .padding(.horizontal, 5)
                .padding(.top, 5)
        } else {
            Text("middle lenght text: \(item)")
                .lineLimit(2)
                .minimumScaleFactor(0.8)
                .multilineTextAlignment(.center)
                .padding(.horizontal, 5)
                .padding(.top, 5)
        }
    }
    .background(.teal)
    .shadow(color: .black.opacity(0.3), radius: 10, x: 0, y: 10)
    

    The spacer pushes the image and text apart from each other, making sure that the images always align at the top.

    enter image description here

    If you are not satisfied with this, then I suggest keeping the GeometryReader. Making the Text 1/4 of the height of the VStack is very reasonable.

    Otherwise, you can put an invisible 2-line text, and overlay the actual text on top of it. IMO, this is more of a hack when compared to GeometryReader.

    VStack{
        Image(systemName: "apple.logo")
            .resizable()
            .scaledToFit()
            .foregroundStyle(.secondary)
        // this takes up 2 lines
        Text("\n")
            .lineLimit(2)
            .minimumScaleFactor(0.8)
            .multilineTextAlignment(.center)
            .padding(.horizontal, 5)
            .padding(.top, 5)
            .frame(maxWidth: .infinity)
            .overlay { // specify the alignment here, if needed
                Group {
                    if item % 2 == 0 {
                        Text("short: \(item)")
                    } else {
                        Text("middle lenght text: \(item)")
                    }
                }
                .lineLimit(2)
                .minimumScaleFactor(0.8)
                .multilineTextAlignment(.center)
                .padding(.horizontal, 5)
                .padding(.top, 5)
            }
    }
    .frame(maxWidth: .infinity, maxHeight: .infinity)
    .aspectRatio(1, contentMode: .fit)
    .background(.teal)
    .shadow(color: .black.opacity(0.3), radius: 10, x: 0, y: 10)
    

    enter image description here