Apr 14, 2024 iOS

SwiftUI HStack with wrap and dynamic height


In SwiftUI, achieving a horizontally stacked layout with wrapping behavior (similar to UIStackView with UIStackViewDistribution.fillEqually and UIStackViewAxis.horizontal in UIKit) along with dynamic height can be a bit tricky because SwiftUI’s built-in layout system doesn’t directly support wrapping behavior. However, you can achieve a similar effect using a combination of GeometryReader, PreferenceKey, and custom view layout.

Here’s a basic implementation of an HStack with wrapping behavior and dynamic height:

import SwiftUI

struct ContentView: View {
    let items = (1...20).map { "Item \($0)" } // Sample items

    var body: some View {
        VStack(alignment: .leading) {
            WrappedHStack(items: items, maxWidth: UIScreen.main.bounds.width) // Adjust maxWidth as needed
                .padding(.horizontal)
            Spacer()
        }
        .padding()
    }
}

struct WrappedHStack: View {
    let items: [String]
    let maxWidth: CGFloat

    var body: some View {
        GeometryReader { geometry in
            self.generateContent(in: geometry)
        }
    }

    private func generateContent(in geometry: GeometryProxy) -> some View {
        var width: CGFloat = 0
        var height: CGFloat = 0

        return ZStack(alignment: .topLeading) {
            ForEach(items, id: \.self) { item in
                Text(item)
                    .padding(.horizontal, 8)
                    .padding(.vertical, 4)
                    .background(Color.blue)
                    .foregroundColor(.white)
                    .cornerRadius(4)
                    .alignmentGuide(.leading, computeValue: { d in
                        if (abs(width - d.width) > maxWidth) {
                            width = 0
                            height -= d.height
                        }
                        let result = width
                        if item == self.items.last! {
                            width = 0 //last item
                        } else {
                            width -= d.width
                        }
                        return result
                    })
                    .alignmentGuide(.top, computeValue: {d in
                        let result = height
                        if item == self.items.last! {
                            height = 0 //last item
                        }
                        return result
                    })
            }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

In this example:

  • ContentView contains the main view hierarchy. It includes a VStack with a WrappedHStack.
  • WrappedHStack is a custom view that takes an array of items and a maximum width. It uses GeometryReader to measure available space and lays out the items accordingly. It calculates the width and height of each item, and if the width exceeds the available space, it wraps the items onto the next line.
  • Each item is represented as a Text view with some styling applied.

This implementation provides a horizontally wrapped layout with dynamic height based on the content. Adjust the padding, spacing, and styling to match your requirements.

2. Using a Third-Party Library (Optional):

Libraries like WrappingHStack (https://github.com/dkk/WrappingHStack) offer a pre-built solution with similar functionality. These libraries often provide additional features or customization options.

Usage example (assuming you downloaded the library):

import WrappingHStack

struct MyView: View {
    let items = ["Item 1", "This is a longer item", "Item 3"]

    var body: some View {
        WrappingHStack(models: items) { text in
            Text(text)
                .padding(10)
                .background(Color.gray.opacity(0.2))
        }
    }
}

Choosing the right method:

  • If you need more control over the layout or prefer a lightweight solution, consider implementing the custom approach.
  • If you want a quicker solution and don’t mind adding a dependency, using a third-party library can be convenient.

Both methods achieve an HStack with wrapping and dynamic height in SwiftUI. Select the approach that best suits your project’s requirements and preferences.