SwiftUI: buttons inside a HStack cause the overall HStack to exceed the bounds

I have a SwiftUI which holds a number of buttons inside of an HStack. These buttons have both an icon and some text, laid out vertically. I am running into the problem where the buttons can become too wide: the HStack is going outside of the bounds of the view itself. It would be logical if the "Download all" button would lay its text on two lines for example, but it's not doing that.

Preview example:

enter image description here

As you can see, the first version has the problem, the three buttons don't fit anymore. But even in the second example the rounded corners don't completely show - only the third example is 100% showing correctly.

Code:

import SwiftUI

struct TransferDetailsButtonsView: View {
  enum ButtonType: Hashable {
    case share
    case download
    case delete

    fileprivate var imageName: String {
      switch self {
        case .share:
          return "icon-share"
        case .download:
          return "icon-download"
        case .delete:
          return "icon-delete"
      }
    }

    fileprivate var title: String {
      switch self {
        case .share:
          return "Share"
        case .download:
          return "Download all"
        case .delete:
          return "Delete"
      }
    }
  }

  /// The button types you want to show
  var buttonTypes: [ButtonType] = [.share, .download, .delete]

  /// The action for the buttons
  var action: (ButtonType) -> Void = { _ in }

  var body: some View {
    HStack(spacing: 0) {
      Spacer(minLength: 20)
        .frame(maxWidth: .infinity)

      ForEach(buttonTypes, id: \.self) { button in
        Button {
          action(button)
        } label: {
          VStack(spacing: 8) {
            Image(button.imageName)
            Text(button.title)
              .lineLimit(nil)
          }
          .fixedSize()
        }

        Spacer(minLength: 20)
          .frame(maxWidth: .infinity)
      }
    }
    .padding(.vertical, 12)
    .foregroundColor(.white)
    .background(RoundedRectangle(cornerRadius: 16).fill(.blue))
  }
}

struct TransferDetailsButtonsView_Previews: PreviewProvider {
  static var previews: some View {
    Group {
      TransferDetailsButtonsView()
        .frame(width: 260)
        .previewLayout(.sizeThatFits)

      TransferDetailsButtonsView()
        .frame(width: 300)
        .previewLayout(.sizeThatFits)

      TransferDetailsButtonsView()
        .frame(width: 420)
        .previewLayout(.sizeThatFits)
    }
  }
}

How can I make it so that the HStack doesn't go outside of the overall bounds, but instead will use multiline text for the button texts?


Solution 1:

Your fixedSize() was making it draw the HStack outside of it's bounds. You want the Texts to fill the available space, then SwiftUI will try to break on the words. If the container is too small it will break within the words so you need to be aware of this, 260 is about the smallest it can go with this font size.

Here's what I came up with, modified to be runnable with an SF symbol. You need some padding in between texts otherwise they will be right up against each other at some sizes of the container.

struct TransferDetailsButtonsView: View {
    enum ButtonType: Hashable {
        case share
        case download
        case delete
        
        fileprivate var imageName: String {
            switch self {
            case .share:
                return "square.and.arrow.up.fill"
            case .download:
                return "square.and.arrow.up.fill"
            case .delete:
                return "square.and.arrow.up.fill"
            }
        }
        
        fileprivate var title: String {
            switch self {
            case .share:
                return "Share"
            case .download:
                return "Download all"
            case .delete:
                return "Delete it now"
            }
        }
    }
    
    /// The button types you want to show
    var buttonTypes: [ButtonType] = [.share, .download, .delete]
    
    /// The action for the buttons
    var action: (ButtonType) -> Void = { _ in }
    
    var body: some View {
        HStack(alignment: .top, spacing: 0) {
            ForEach(buttonTypes, id: \.self) { button in
                Button {
                    action(button)
                } label: {
                    VStack(spacing: 8) {
                        Image(systemName: button.imageName)
                        Text(button.title)
                            .frame(maxWidth: .infinity)
                    }
                }
                .padding(.horizontal, 4)
            }
        }
        .padding(.vertical, 12)
        .foregroundColor(.white)
        .background(RoundedRectangle(cornerRadius: 16).fill(.blue))
    }
}

enter image description here