How to hide clipped views in SwiftUI HStack

Viewed 1511

Is there a possibility in SwiftUI to show only as many (fixed size) views in one row as fit without clipping, e.g. cutting of parts of the view.

Example Layout

 ----------------------------------------- 
|                                         |
|  -------     -------     -------     -------     -------
| | VIEW1 |   | VIEW2 |   | VIEW3 |   | VIEW4 |   | VIEW5 |
|  -------     -------     -------     -------     -------
|                                         |
 -----------------------------------------

In this example VIEW5 should be hidden, because it is completely out of bounds with respect to the parent view. This could be done with .clipped().

VIEW4 should also be completely hidden! Because if it would be shown, it must be cut off. All other views should be rendered normally.

SwiftUI Layout

This initial try has the following problems:

  • since the width of the itemView is much larger then the outer VStack, the leading edges are not aligned => ☑️ Solution
  • ❌The last view might be visible even if it is not fully on screen. This should be avoided.
struct DemoView: View {
    let items: [String]
    @State private var totalHeight = CGFloat.zero
    
    var body: some View {
        VStack(alignment: .leading) {
            Text("Demo View:")
            itemView
                .padding()
                .background(Color.green)
        }
    }
    
    private var itemView: some View {
        HStack {
            ForEach(items, id: \.self) { item in
                Text(item)
                    .lineLimit(1)
                    .fixedSize()
                    .padding(.all, 5)
                    .font(.body)
                    .background(Color.blue)
                    .foregroundColor(Color.white)
                    .cornerRadius(5)
            }
        }
        .frame(maxWidth: 350)
    }
}

#if DEBUG
struct DemoView_Previews: PreviewProvider {
    static var items: [String] = (2000..<2020).map(String.init)
//        .map { item in
//            Bool.random() ? item : item + item
//        }
    
    static var previews: some View {
        DemoView(items: items)
//            .previewLayout(.sizeThatFits)
    }
}
#endif

Wrong Layout Example

3 Answers

Use clipped with alignment on HStack frame (tested with Xcode 12.1 / iOS 14.1)

private var itemView: some View {
    HStack {
        ForEach(items, id: \.self) { item in
            Text(item)
                .lineLimit(1)
                .fixedSize()
                .padding(.all, 5)
                .font(.body)
                .background(Color.blue)
                .foregroundColor(Color.white)
                .cornerRadius(5)
        }
    }
    .frame(maxWidth: 350, alignment: .leading)
    .clipped()
}

Here is a solution based on your comments. The VStack is aligned to leading, and then I added a ScrollView to hold the HStack of items. Further, I added a white .overlay to each item where the opacity will change from 1.0 (full covering the item) to 0.0 (showing the item) when the item moves fully onto screen.

Also note that I moved the leading padding to within the VStack, this way when you scroll to the left, the items move to the edge of the screen instead of getting cut off where the padding was.

struct DemoView: View {
    let items: [String]
    
    var body: some View {
        VStack(alignment: .leading) {
            Text("Demo View:")
                .padding(.leading)

            ScrollView(.horizontal, showsIndicators: false) {
                HStack {
                    ForEach(items, id: \.self) { item in
                        Text(item)
                            .lineLimit(1)
                            .fixedSize()
                            .padding(.all, 5)
                            .font(.body)
                            .background(Color.blue)
                            .foregroundColor(Color.white)
                            .cornerRadius(5)
                            .overlay(
                                GeometryReader { geometry in
                                    Color.white
                                        .opacity(
                                            geometry.frame(in: .global).maxX < UIScreen.main.bounds.width ? 0.0 : 1.0
                                        )
                                }
                            )
                    }
                }
                .background(Color.orange) // Starting HStack
                .padding(.leading)
                .background(Color.green) // Scrollable area
            }
        }
    }
    
}

struct DemoView_Previews: PreviewProvider {
    static var previews: some View {
        DemoView(items: [
            "ONE",
            "TWO",
            "THREE",
            "FOUR",
            "FIVE",
            "SIX",
            "SEVEN",
            "EIGHT",
            "NINE",
            "TEN",
        ])
    }
}

This is the answer from nicksamo with generic content ViewBuilder. Thank you for this solution! :)

import SwiftUI

struct NonClippingView<Item: Hashable, Content: View>: View {
    let items: [Item]
    let isScrollingDisabled: Bool
    let backgroundColor: Color
    let content: (Item) -> Content

    init(items: [Item], isScrollingDisabled: Bool = false, backgroundColor: Color = .white, @ViewBuilder content: @escaping (Item) -> Content) {
        self.items = items
        self.isScrollingDisabled = isScrollingDisabled
        self.backgroundColor = backgroundColor
        self.content = content
    }
    
    var body: some View {
        GeometryReader { proxy in
            ScrollView(.horizontal, showsIndicators: false) {
                HStack {
                    ForEach(items, id: \.self) { item in
                        content(item)
                            .overlay(
                                GeometryReader { geometry in
                                    backgroundColor
                                        .opacity(
                                            geometry.frame(in: .global).maxX < proxy.size.width ? 0.0 : 1.0
                                        )
                                }
                            )
                    }
                }
//                .background(Color.orange) // Starting HStack
                .padding(.leading)
//                .background(Color.green) // Scrollable area
            }
            .disabled(isScrollingDisabled)
        }
    }
    
}

struct NonClippingView_Previews: PreviewProvider {
    static var previews: some View {
        NonClippingView(items: [
            "ONE",
            "TWO",
            "THREE",
            "FOUR",
            "FIVE",
            "SIX",
            "SEVEN",
            "EIGHT",
            "NINE",
            "TEN",
        ], content: { item in
            VStack {
                Image(systemName: "tag")
                Text(item)
                    .lineLimit(1)
                    .fixedSize()
            }
            .padding(.all, 5)
            .font(.body)
            .background(Color.blue)
            .foregroundColor(Color.white)
            .cornerRadius(5)
        })
    }
}
Related