NEW: Learn to build the incredible iOS 15 Weather app today! >>

SOLVED: Any way to get HStack to fill proportionally like UIStackView?

Forums > SwiftUI

In my app, I have a bunch of images for displaying letters. The images comes in 2 different sizes, depending on how many letters are in the image.

The problem: I can't get HStack to resize the images proportionally so that images of two different widths have the same height.

Github sample project

The UIStackView is straightforward to setup:

 // create temp image array
        var imageArray: [UIImageView] = []

        for item in graphemes {
            // create the image view
            let imageView = UIImageView()
            // enable auto layout
            imageView.translatesAutoresizingMaskIntoConstraints = false

            // get the image
            guard let image = UIImage(named: item) else { return }
            // assign the image to the imageView
            imageView.image = image

            // set an aspect ratio constraint
//            imageView.widthAnchor.constraint(equalTo: imageView.heightAnchor, multiplier: image.size.width / image.size.height).isActive = true

            imageView.contentMode = .scaleAspectFit

            imageArray.append(imageView)
        }

        let stackView = UIStackView(arrangedSubviews: imageArray)
        stackView.translatesAutoresizingMaskIntoConstraints = false
        stackView.axis = .horizontal
        stackView.distribution = .fillProportionally
        stackView.alignment = .center

        view.addSubview(stackView)

The problematic HStack:

 HStack {
                ForEach(0 ..< graphemes.count, id:\.self) { index in
                    Image(graphemes[index])
                        .resizable()
                        .scaledToFit()
                }
            }
            .frame(maxWidth: .infinity, maxHeight: 100)
            .background(Color.green)

Things I've tried:

  • Layout priorities: if I set them uniformly on the images, nothing changes. If I set them only on one size of image or the other, the other size disappears completely.
  • FixedSize(): using this can get the size uniform, but when there are too many images they'll just go outside the visible area.

Idea I just can't get to work:

  • PreferenceKey: I've tried following a few different articles and can't get PreferenceKeys to work properly.

Any ideas on how I can get this HStack to play along properly? I'd really like to stick with a pure SwiftUI solution if I can.

   

I think of ForEach as a small factory. It creates a bunch of views.

In your case, it's creating Images containing graphemes. Are they coming out of your factory in different sized boxes? In your code you set the frame on the HStack. This is setting the size of a railway boxcar. But the grapheme boxes inside the boxcar are all different sizes. Did you consider putting frames on each grapheme view?

 HStack {
     ForEach(0 ..< graphemes.count, id:\.self) { index in
          Image(graphemes[index])
          .frame(maxWidth: .infinity, maxHeight: 100) // Give each grapheme view a consistent frame.
          .resizable()
          .scaledToFit()
     }
}

You may be interested in SwiftUI programmer CodeSlicing's library named PureSwiftUI.

See: Code Slicing

His library addresses many SwiftUI holes, or tries to make portions easier to use. In particular, he provides View extentions for components that don't expand to fill all available space by default. His example:

// Text is a good example of components that don't expand to fill all space by default. 
// Ordinarily you would achieve this like so:

// Apple SwiftUI
Text("Some expanding text")
    .frame(maxWidth: .infinity, maxHeight: .infinity) // Not very clear to new programmers.

// PureSwifUI This is accomplished in the following way:
Text("Some expanding text")
    .greedyFrame() // or .greedyWidth / .greedyHeight

// PureSwiftUI provides this extension to more clearly express the design's intent. 
// You might use .greedyHeight

His library may help you? Your mileage may vary.

   

Thanks for the reply and the info about PureSwiftUI.

Unfortunately, applying frames to each of the Images directly doesn't work. I can see in the preview that the frames are being modified (e.g. maxHeight: 100 makes them all uniform height), but that doesn't change the resizing of the image itself -- it just affects the empty space around the image.

maxWidth: .infinity doesn't change the width of the frames -- they all stay an equal width with or without that modifier.

   

Can you post a link to these images? Hard to try out options without them.

   

Here's the test project on GitHub if you want to play around with both the SwiftUI and UIKit versions:

Test project link

The project launches into the view you see in the first post. The "a" and "bb" at the bottom are buttons that add more images to display, which helps for testing how things will scale when the words are longer.

If you just want the images:

"a" image

bb

   

LazyHStack or LazyVStack an option?

   

I could use a Lazy stack, but it's not clicking with me how that would help solve my problem. Any suggestions on an approach I could take?

   

After reading up more on HStack, it really doesn't seem like it's possibility in the current incarnation of SwiftUI. However, something tickled the back of my brain, and I remembered that something does allow for variable view width: LazyVGrid.

With that in mind, I was able to make something work! It relies on dynamically generated columns and calculating view size based on contents and available space. It's not the prettiest, but hopefully this helps someone in the future :)

struct ContentView: View {

    @State private var graphemes: [String] = ["aSound", "bb", "aSound", "bb", "aSound", "bb", "aSound", "bb"]
    @State private var viewSize = CGSize()

    var body: some View {
        VStack {
            // spacer to center the view
            Spacer()

                LazyVGrid(columns: columns, alignment: .center, spacing: 0) {
                    ForEach(0 ..< graphemes.count, id:\.self) {index in
                        Image(graphemes[index])
                            .resizable()
                            .scaledToFit()
                    }
                }
                // take up all available horizontal space and cap the view at 100 (the natural size of the images)
                .frame(maxWidth: .infinity, maxHeight: 100)
                .background(
                    // put a geometry reader in the background to read the maximum size of the view
                    GeometryReader { geo in
                        Color.purple
                            .onAppear {
                                // assign the view size to a state variable
                                viewSize = geo.size
                            }
                    }
                )

            // spacer to center the view
            Spacer()
        }
    }

    private var columns: [GridItem] {
        // create a variable to capture the total width of all the images in the array
        var totalCharacterWidth: CGFloat = 0

        // add up the widths of all characters based on their sizes
        for grapheme in graphemes {
            if grapheme == "bb" {
                totalCharacterWidth += 100
            } else {
                totalCharacterWidth += 58.3
            }
        }

        // calculate proportional widths for the two sizes of images based on the widths of all images and the available space
        let fullCharacterWidth = 100 * viewSize.width / totalCharacterWidth
        let smallCharacterWidth = 58.3 * viewSize.width / totalCharacterWidth

        // create a array to hold all the GridItems
        var array = [GridItem]()

        // The max size of the image should be the smaller of its calculated proportional width or the max height of the view
        let maxFullWidth = min(fullCharacterWidth, viewSize.height)
        let maxSmallWidth = min(smallCharacterWidth, viewSize.height * 0.583)

        // fill the GridItem array with fixed-size items of the appropriate size
        for grapheme in graphemes {
            let item = GridItem(.fixed(grapheme == "bb" ? maxFullWidth : maxSmallWidth), spacing: 0)

            array.append(item)
        }

        return array
    }
}

   

Hacking with Swift is sponsored by Essential Developer

SPONSORED Learn the most up-to-date techniques and strategies for testing new and legacy Swift code in this free practical course for iOS devs who want to become complete Senior iOS Developers.

Learn more

Sponsor Hacking with Swift and reach the world's largest Swift community!

Reply to this topic…

You need to create an account or log in to reply.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.