iosswiftxcodeswiftui

SwiftUI shrink view on dragging edges


I am trying to implement a video trimmer UI in SwiftUI as follows:

 struct SimpleTrimmer: View {
    @State private var startPosition: CGFloat = 0
    @GestureState private var isDragging: Bool = false
    
    @State private var lastStartPosition: CGFloat = .zero
    @State private var frameWidth:CGFloat = 300
    
    var body: some View {
        HStack(spacing: 10) {
                Image(systemName: "chevron.compact.left")
                    .frame(height:70)
                    .frame(width:20)
                    .padding(.horizontal, 5)
                    .background(Color.blue)
                    .offset(x: startPosition)
                    .gesture(
                        DragGesture(minimumDistance: 0)
                            .updating($isDragging, body: { value, out, transaction in
                                out = true
                                
                            })
                            .onChanged { value in
                                let translation = value.translation.width
                                startPosition = translation + lastStartPosition
                            }.onEnded { _ in
                                lastStartPosition = startPosition
                                NSLog("Last start position \(lastStartPosition)")
                            }
                    )
            
                
                Spacer()
                
                Image(systemName: "chevron.compact.right")
                .frame(height:70)
                .frame(width:20)
                .padding(.horizontal, 5)
                .background(Color.blue)
            }
            .foregroundColor(.black)
            .font(.title3.weight(.semibold))
            .padding(.horizontal, 7)
            .padding(.vertical, 3)
            .background(.yellow)
            .clipShape(RoundedRectangle(cornerRadius: 7))
            .frame(width: frameWidth)
          //  .offset(x: startPosition)
            .onGeometryChange(for: CGFloat.self) { proxy in
                proxy.size.width
            } action: { width in
                print("width = \(width)")
            }
    }
}

enter image description here

This works for moving the left hand of the trimmer when dragged. What I need is also to shrink the SimpleTrimmer view as the ends are dragged. It simply doesn't work no matter what I do (such as adjusting the offset and width of the main HStack, etc).

EDIT: I tried incorporating boundary conditions for left trimmer. Trimming is very jittery as the left trimmer reaches boundary of right trimmer.

    let minWidth: CGFloat = 10
    ....
    ....

    HStack(spacing: 10) {
            Image(systemName: "chevron.compact.left")
                .frame(width: 30, height: 70)
                .background(Color.blue)
                .offset(x: leftAdjustment)
                .gesture(
                    DragGesture(minimumDistance: 0)
                        .updating($leftDragOffset) { value, state, trans in
                            var dragWidth = value.translation.width
                            
                            if dragWidth + leftAdjustment > frameWidth - rightAdjustment - minWidth {
                                dragWidth = frameWidth - rightAdjustment - minWidth - leftAdjustment
                            }
                            
                            state = dragWidth
                        }
                        .onEnded { value in
                            leftOffset = max(0, leftOffset + value.translation.width)
                        }
                )

Solution

  • In order for the background frame to change in coordination with the drag gesture, you need to adjust the width of the background as the drag happens. At the moment, the width is fixed, so no changes are seen.

    I would suggest the following changes:

    Here is an updated version:

    struct SimpleTrimmer: View {
        let frameWidth:CGFloat = 300
        @State private var leftPadding: CGFloat = 0
        @GestureState private var dragOffset: CGFloat = 0
    
        private var leadingPadding: CGFloat {
            max(0, leftPadding + dragOffset)
        }
    
        var body: some View {
            HStack(spacing: 10) {
                Image(systemName: "chevron.compact.left")
                    .frame(width: 30, height: 70)
                    .background(Color.blue)
                    .padding(.leading, leadingPadding)
                    .gesture(
                        DragGesture(minimumDistance: 0)
                            .updating($dragOffset) { value, state, trans in
                                state = value.translation.width
                            }
                            .onEnded { value in
                                leftPadding = max(0, leftPadding + value.translation.width)
                            }
                    )
    
                Spacer()
    
                Image(systemName: "chevron.compact.right")
                    .frame(width: 30, height: 70)
                    .background(Color.blue)
            }
            .foregroundColor(.black)
            .font(.title3.weight(.semibold))
            .padding(.horizontal, 7)
            .padding(.vertical, 3)
            .background {
                RoundedRectangle(cornerRadius: 7)
                    .fill(.yellow)
                    .padding(.leading, leadingPadding)
            }
            .frame(width: frameWidth)
        }
    }
    

    Animation


    EDIT Following from your comment, I tried adding a drag gesture to the trimmer (chevron) on the right, hoping that the trailing padding could be adjusted in the same way as the leading padding above.

    When I tried it, the movement of the trimmer was jittery. I think the reason for this is because there is an inter-dependency between the size of the spacer and the padding on the trimmers. Although it seemed to work for the case of the leading padding, it doesn't work well with trailing padding.

    It works better if you set an x-offset on the trimmers, like you were originally using. This means finding better names for the state variables too!

    Here is an updated version with drag gestures on both trimmers:

    struct SimpleTrimmer: View {
        let frameWidth:CGFloat = 300
        @State private var leftOffset: CGFloat = 0
        @State private var rightOffset: CGFloat = 0
        @GestureState private var leftDragOffset: CGFloat = 0
        @GestureState private var rightDragOffset: CGFloat = 0
    
        private var leftAdjustment: CGFloat {
            max(0, leftOffset + leftDragOffset)
        }
    
        private var rightAdjustment: CGFloat {
            max(0, rightOffset - rightDragOffset)
        }
    
        var body: some View {
            HStack(spacing: 10) {
                Image(systemName: "chevron.compact.left")
                    .frame(width: 30, height: 70)
                    .background(Color.blue)
                    .offset(x: leftAdjustment)
                    .gesture(
                        DragGesture(minimumDistance: 0)
                            .updating($leftDragOffset) { value, state, trans in
                                state = value.translation.width
                            }
                            .onEnded { value in
                                leftOffset = max(0, leftOffset + value.translation.width)
                            }
                    )
    
                Spacer()
    
                Image(systemName: "chevron.compact.right")
                    .frame(width: 30, height: 70)
                    .background(Color.blue)
                    .offset(x: -rightAdjustment)
                    .gesture(
                        DragGesture(minimumDistance: 0)
                            .updating($rightDragOffset) { value, state, trans in
                                state = value.translation.width
                            }
                            .onEnded { value in
                                rightOffset = max(0, rightOffset - value.translation.width)
                            }
                    )
            }
            .foregroundColor(.black)
            .font(.title3.weight(.semibold))
            .padding(.horizontal, 7)
            .padding(.vertical, 3)
            .background {
                RoundedRectangle(cornerRadius: 7)
                    .fill(.yellow)
                    .padding(.leading, leftAdjustment)
                    .padding(.trailing, rightAdjustment)
            }
            .frame(width: frameWidth)
        }
    }