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)")
}
}
}
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)
}
)
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:
Use the GestureState
variable to record the drag offset. This is automatically reset to 0 when the drag gesture ends, which is convenient.
Use the drag offset to control leading padding, instead of applying an x-offset.
The same leading padding should be applied to the trimmer (chevron) as well as to the filled background.
The filled background can be drawn as a RoundedRectangle
, instead of using a clip shape. This makes it easier to apply the padding.
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)
}
}
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)
}
}