SwiftUI HStack clipping subviews

126 Views Asked by At

I am trying to implement a video scrubber/trimmer in SwiftUI. To do this, put image thumbnails in an HStack as follows:

struct ThumbnailsView: View {
  @State private var thumbImages:[ThumbImage] = []

   var body: some View {
      HStack(alignment: .center, spacing: 0) {
          ForEach(thumbImages, id: \.time) { thumbImage in
              Image(uiImage: thumbImage.image)
                  .border(Color.white, width: 5)
           }
           .padding(5)
       }
       
    }
 }

 struct ThumbImage {
   var time: CMTime
   let image: UIImage
 }

This should basically allow to construct series of images in the scrubber/trimmer (like in Photos app). However, my requirement (which is different from behavior in Photos app) is that as user trims from either end, the trimmer gets shortened and clips the leftmost(or rightmost depending on direction of trim) thumbnail by respective amount, so that only a fraction of the leftmost or rightmost thumbnail is shown as the user drags the end of the trimmer. I could do it in UIKit but just want to understand how this can be done in SwiftUI.

enter image description here

2

There are 2 best solutions below

3
ChrisR On BEST ANSWER

Here is a quick demo for what I think you are looking for. It's gonna get a bit more complicated when inside a ScollView, but you have a starting point.

enter image description here

struct ContentView: View {
    
    @State private var timecode = 200.0
    @State private var dragAmount = 0.0
    
    var body: some View {
        VStack(alignment: .leading) {
            Image("thumbnail")
                .resizable()
                .scaledToFit()
            
            Spacer()
            
            HStack(spacing: 0) {
                ForEach(0..<3) { _ in
                    Image("thumbnail")
                        .resizable()
                        .scaledToFit()
                        .frame(width: 120) // this one is important
                }
                
            }
            .border(.blue)
            .frame(width: timecode + dragAmount, alignment: .leading)
            .clipped()
            
            .overlay(alignment: .leading) {
                Rectangle()
                    .fill(.yellow)
                    .frame(width: 10, alignment: .leading)
                    .offset(x: timecode + dragAmount)
                
                    .gesture(DragGesture()
                        .onChanged { value in
                            dragAmount = value.translation.width
                        }
                        .onEnded { value in
                            timecode += value.translation.width
                            dragAmount = 0
                        }
                             , including: .gesture)
            }
            Spacer()
            Text("timecode = \(timecode, specifier: "%.1f")")
                .frame(maxWidth: .infinity, alignment: .center)
        }
    }
}
1
ChrisR On

As for your extended question ... the rest you might need to solve yourself:

struct ContentView: View {
    
    @State private var timecodeStart = 100.0
    @State private var dragAmountStart = 0.0
    @State private var timecodeEnd = 300.0
    @State private var dragAmountEnd = 0.0
    
    var body: some View {
        VStack(alignment: .leading) {
            Image("thumbnail")
                .resizable()
                .scaledToFit()
            
            Spacer()
            
            ScrollView(.horizontal) {
                HStack(spacing: 0) {
                    
                    HStack(spacing: 0) {
                        ForEach(0..<10) { _ in
                            Image("thumbnail")
                                .resizable()
                                .scaledToFit()
                                .frame(width: 120) // this one is important
                        }
                    }
                    .border(.blue)
                    .frame(width: timecodeEnd + dragAmountEnd - timecodeStart - dragAmountStart, alignment: .leading)
                    .offset(x: -timecodeStart - dragAmountStart)
                    .clipped()
                    
                    Color.black
                        .frame(width: 500, height: 68)
                }
                .offset(x: timecodeStart + dragAmountStart)

                .overlay(alignment: .leading) {
                    // Front
                    Rectangle()
                        .fill(.yellow)
                        .frame(width: 10, alignment: .leading)
                        .offset(x: timecodeStart + dragAmountStart)
                    
                        .gesture(DragGesture(minimumDistance: 0)
                            .onChanged { value in
                                dragAmountStart = value.translation.width
                            }
                            .onEnded { value in
                                timecodeStart += value.translation.width
                                dragAmountStart = 0
                            }
                        )
                    
                    // End
                    Rectangle()
                        .fill(.yellow)
                        .frame(width: 10, alignment: .leading)
                        .offset(x: timecodeEnd + dragAmountEnd)
                    
                        .gesture(DragGesture(minimumDistance: 0)
                            .onChanged { value in
                                dragAmountEnd = value.translation.width
                            }
                            .onEnded { value in
                                timecodeEnd += value.translation.width
                                dragAmountEnd = 0
                            }
                        )

                }
            }
            Spacer()
            Text("from \(timecodeStart, specifier: "%.1f") to \(timecodeEnd, specifier: "%.1f")")
                .frame(maxWidth: .infinity, alignment: .center)
        }
    }
}