0

I am using a ScrollView and a LazyHStack to create a horizontal carousel.

The goal I want to achieve is this:

SwiftUI Carousel ScrollView LazyHStack variable height

What I want is:

  1. Image which has the same width and height
  2. The text below the image can be 1,2 or 3 lines
  3. The image should all be in the same line in terms of alignment
  4. The first line of text should all start on the same line in terms of alignment

What I am able to achieve so far is this:

SwiftUI Carousel ScrollView HStack variable size

As you can see, the images don't align and neither does the text due to that.

Here is my code

The carousel:

struct CarouselView<Content: View>: View {
    let content: Content
    @State private var currentIndex = 0
    @State private var contentSize: CGSize = .zero
    
    private let showsIndicators: Bool
    private let spacing: CGFloat
    private let shouldSnap: Bool
    
    init(showsIndicators: Bool = true,
         spacing: CGFloat = .zero,
         shouldSnap: Bool = false,
         @ViewBuilder content: @escaping () -> Content) {
        self.content = content()
        self.showsIndicators = showsIndicators
        self.spacing = spacing
        self.shouldSnap = shouldSnap
    }
    
    var body: some View {
        ScrollView(.horizontal, showsIndicators: showsIndicators) {
            LazyHStack(spacing: spacing) {
                content
            }.apply {
                if #available(iOS 17.0, *), shouldSnap {
                    $0.scrollTargetLayout()
                } else {
                    $0
                }
            }
        }
        .apply {
            if #available(iOS 17.0, *), shouldSnap {
                $0.scrollTargetBehavior(.viewAligned)
            } else {
                $0
            }
        }
    }
}

extension View {
    func apply<V: View>(@ViewBuilder _ block: (Self) -> V) -> V { block(self) }
}

And then I use the carousel like this:

struct ContentView: View {
    
    let imagesNames = ["img-1", "img-2", "img-3", "img-4"]
    let numberOfLines = [2, 1, 3, 2]
    
    var body: some View {
        
        VStack(spacing: 40) {
            continuousCarousel
        }
        
    }
    
    private var continuousCarousel: some View {
        CarouselView(showsIndicators: true,
                     spacing: 20) {
            ForEach(0 ..< imagesNames.count, id: \.self) { index in
                createImageTile(with: imagesNames[index],
                                height: 70,
                                numberOfLines: numberOfLines[index])
            }
        }
    }
    
    private func createImageTile(with image: String,
                                 height: CGFloat,
                                 numberOfLines: Int) -> some View {
        VStack(spacing: .zero) {
            Image(image)
                .resizable()
                .cornerRadius(30)
                .scaledToFit()
                .aspectRatio(contentMode: .fill)
                .frame(width: 200, height: 100)
                .padding(.bottom, 30)
            
            Text("Headline")
                .bold()
                .padding(.bottom, 10)
            
            ForEach(0 ..< numberOfLines, id: \.self) { _ in
                Text("Some description here")
                    .padding(.bottom, 5)
            }
        }
    }
}

I feel like applying an alignment somewhere or a spacer might fix this, but I don't know where.

Applying a bottom alignment on my lazyHStack didn't work as it moved the main content region to the bottom of the scrollview.

How can I achieve what I need ?

Update 1

After giving the first answer below a go, adding a spacer to the VStack, did help align everything to the top as follows:

private func createImageTile(with image: String,
                             height: CGFloat,
                             numberOfLines: Int) -> some View {
    VStack(spacing: .zero) {
        Image(image)
            .resizable()
            .cornerRadius(30)
            .scaledToFit()
            .aspectRatio(contentMode: .fill)
            .frame(width: 200, height: 100)
            .padding(.bottom, 30)
        
        Text("Headline")
            .bold()
            .padding(.bottom, 10)
        
        ForEach(0 ..< numberOfLines, id: \.self) { _ in
            Text("Some description here")
                .padding(.bottom, 5)
        }
        
        Spacer() // added this
    }
}

However, this introduced another issue, the top of my image is getting clipped.

enter image description here

I tried adding the clipped modifier to the scrollview as suggested here, however, that didn't change anything.

If I add a top spacing to the VStack, this could work but the value seems arbitrary and seems more of a hack than a solution.

1 Answer 1

0

You can try

VStack(spacing: .zero) { // .leading align content to the left
       Image(image)
           .resizable()
           .cornerRadius(30)
           .scaledToFit()
           .aspectRatio(contentMode: .fill)
           .frame(width: 200, height: 100)
           .padding(.bottom, 30)
           Text("Headline")
               .bold()
               .padding(.bottom, 10)
           ForEach(0 ..< numberOfLines, id: \.self) { _ in
               Text("Some description here")
                   .padding(.bottom, 5)
           }
        Spacer()  // Align vertical content to top
}
Sign up to request clarification or add additional context in comments.

3 Comments

Thanks, while this does work, for some reason, the top of my image is getting clipped as well: prnt.sc/AxYS7MBDwY7I
Can you give it a top padding as well
Giving it a top padding does help, but this seems like a hack as I don't get why we should need it ? Why would the Image going outside the VStack's bounds ?

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.