79222114

Date: 2024-11-25 08:04:22
Score: 2
Natty:
Report link

I ended up making a custom AdaptiveVGrid implementation, loosely based on this tutorial: https://www.fivestars.blog/articles/adaptive-swiftui-views/

I'm very new to SwiftUI, so there's likely some issues with the implementation. Take it with a grain of salt. It's quite verbose at least.

I'll leave this question open in case anyone has a better approach.

/// A view to display items in a grid, while adapting to the available space, item size, and number of items.
/// When items can fit in one row or column, prefer that (depending on whether we have more horizontal or vertical space).
/// Otherwise uses a LazyVGrid insize a ScrollView to display the content.
///
/// content should have the number of subviews specified as numItems.
struct AdaptiveVGrid<Content: View>: View {
  var numItems: Int
  var itemMinSize: CGSize
  var itemMaxSize: CGSize
  var itemSpacing: CGFloat
  var content: Content
  
  public init(
    numItems: Int,
    itemMinSize: CGSize,
    itemMaxSize: CGSize,
    itemSpacing: CGFloat,
    @ViewBuilder content: () -> Content
  ) {
    self.numItems = numItems
    self.itemMinSize = itemMinSize
    self.itemMaxSize = itemMaxSize
    self.itemSpacing = itemSpacing
    self.content = content()
  }
  
  var body: some View {
    GeometryReader { geometry in
      bodyImpl(availableSize: geometry.size)
        .frame(minWidth: geometry.size.width, minHeight: geometry.size.height)
    }
  }
  
  @ViewBuilder
  func bodyImpl(availableSize: CGSize) -> some View {
    let widthRatio = availableSize.width / (CGFloat(numItems) * itemMaxSize.width + CGFloat(numItems - 1) * itemSpacing)
    let heightRatio = availableSize.height / (CGFloat(numItems) * itemMaxSize.height + CGFloat(numItems - 1) * itemSpacing)
    
    if widthRatio >= heightRatio && availableSize.width >= (CGFloat(numItems) * itemMinSize.width + CGFloat(numItems - 1) * itemSpacing) {
      HStack(spacing: itemSpacing) { content }
    } else if heightRatio >= widthRatio && availableSize.height >= (CGFloat(numItems) * itemMinSize.height + CGFloat(numItems - 1) * itemSpacing) {
      VStack(spacing: itemSpacing) { content }
    } else {
      ScrollView {
        let columns = [GridItem(.adaptive(minimum: itemMinSize.width, maximum: itemMaxSize.width), spacing: itemSpacing)]
        LazyVGrid(columns: columns, alignment: .center) {
          content
        }
        .frame(minWidth: availableSize.width, minHeight: availableSize.height)
      }
    }
  }
}

struct MyView: View {
  @ObservedObject var viewModel: MyModel
  @ScaledMetric var accessabilityScale: CGFloat = 1
  
  @State private var availableSize: CGSize = .zero
  private let itemMinSizeBase = CGSize(width: 100, height: 100)
  private let itemMaxSizeBase = CGSize(width: 250, height: 250)
  private let itemSpacingBase = 20.0
  
  var body: some View {
    GeometryReader { geometry in
      AdaptiveVGrid(
        numItems: viewModel.items.count,
        itemMinSize: adjustSize(itemMinSizeBase, availableSize: geometry.size),
        itemMaxSize: adjustSize(itemMaxSizeBase, availableSize: geometry.size),
        itemSpacing: itemSpacingBase * accessabilityScale
      ) {
        ForEach(viewModel.items, id: \.id) { item in
          itemView(item: item)
            .frame(minWidth: adjustSize(itemMinSizeBase, availableSize: geometry.size).width,
                   maxWidth: CGFloat.greatestFiniteMagnitude,
                   minHeight: adjustSize(itemMinSizeBase, availableSize: geometry.size).height,
                   maxHeight: adjustSize(itemMaxSizeBase, availableSize: geometry.size).height
            )
        }
        .padding()
      }
    }
  }
  
  private func adjustSize(_ size: CGSize, availableSize: CGSize) -> CGSize {
    CGSize(width: min(size.width, availableSize.width),
           height: min(size.height * accessabilityScale, availableSize.height))
  }
}
Reasons:
  • Blacklisted phrase (1): this tutorial
  • RegEx Blacklisted phrase (1.5): I'm very new
  • Long answer (-1):
  • Has code block (-0.5):
  • Self-answer (0.5):
  • Low reputation (0.5):
Posted by: Lysann Tranvouez