'SwiftUI - Misaligned Rectangles when Animating Size of ZStack
I'm creating a custom "Disclosure Group" in SwiftUI, that implements a "swipe to delete" kind of function. It works by having two rectangles stacked on top of each other, and the one on the bottom is the red "delete" button you see when you swipe. I also have a boolean value that flags whether or not the component is "expanded," i.e, having larger size. Here is a video of what this looks like:
As you can see, the component expands in size when tapped, and shows a red "delete" button when dragged. But, when unexpanding the component, you can see that part of the delete rectangle shows on the bottom. Below is the implementation, and I'm not sure why the two rectangles don't completely stick together. Does anyone know how I can avoid this glitch?
MRE:
import SwiftUI
struct ContentView: View {
var body: some View {
VStack(spacing: 30) {
TestDisclosure()
TestDisclosure()
TestDisclosure()
Spacer()
}
}
}
struct TestDisclosure: View {
@State var expanded: Bool = false
@State var isDeleting: Bool = false
@State var horzdrag: CGFloat = 0 // the horizontal translation of the drag
@State var predictedEnd: CGFloat = 0 // the predicted end translation of the drag
var body: some View {
ZStack {
Rectangle()
.foregroundColor(.red)
label
.clipped()
.offset(x: getOffset(horzdrag: horzdrag))
.animation(.spring(), value: horzdrag)
}
.offset(x: isDeleting ? -400 : 0)
.animation(.spring(), value: isDeleting)
.transition(.move(edge: .leading))
.gesture(
DragGesture()
.onChanged { gesture in
onDragChange(gesture: gesture)
}
.onEnded { _ in
onDragEnd()
}
)
.cornerRadius(15)
.padding(.horizontal)
.onTapGesture {
withAnimation(.spring()) {
expanded.toggle()
}
}
.frame(maxHeight: expanded ? 150 : 85)
.clipped()
}
private var label: some View {
ZStack {
Rectangle()
.foregroundColor(.teal)
VStack {
HStack {
Text("Test")
Spacer()
Text("1 unit")
Text("12 units")
}
.padding(.horizontal)
}
}
}
private func onDragChange(gesture: DragGesture.Value) {
horzdrag = gesture.translation.width
predictedEnd = gesture.predictedEndTranslation.width
}
private func onDragEnd() {
if getOffset(horzdrag: horzdrag) <= -400 {
withAnimation(.spring()) {
isDeleting = true
}
}
horzdrag = .zero
}
// used to calculate how far to move the teal rectangle
private func getOffset(horzdrag: CGFloat) -> CGFloat {
if isDeleting {
return -400
} else if horzdrag < -165 {
return -400
} else if predictedEnd < -60 && horzdrag == 0 {
return -80
} else if predictedEnd < -60 {
return horzdrag
} else if predictedEnd < 50 && horzdrag > 0 && (-80 + horzdrag <= 0) {
return -80 + horzdrag
} else if horzdrag < 0 {
return horzdrag
} else {
return 0
}
}
}
Solution 1:[1]
This looks like interference of same animation (spring in this case) applied to different properties, `cause effect is observed when tap (collapse) is applied when drag (offset) is not yet finished.
I'm not sure if this is a bug, but the workaround is to use type of different animations.
Here is a fix - use default instead of spring.
Tested with Xcode 13.3 / iOS 15.4
label
.clipped()
.offset(x: getOffset(horzdrag: horzdrag))
.animation(.default, value: horzdrag) // << here !!
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 | Asperi |

